From 06dcac4c2f8bcb632acfab2bf9a967af60f090fd Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 19 Oct 2020 15:26:34 +0200 Subject: [PATCH 001/502] fix: remove signing key creation (when not found) --- example/internal/mock/storage.go | 5 +-- pkg/op/mock/storage.mock.go | 22 +++---------- pkg/op/op.go | 54 +------------------------------- pkg/op/signer.go | 3 ++ pkg/op/storage.go | 3 +- pkg/utils/sign.go | 4 +++ 6 files changed, 14 insertions(+), 77 deletions(-) diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index 9671ec7..ffddd28 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -157,15 +157,12 @@ func (s *AuthStorage) CreateToken(_ context.Context, authReq op.TokenRequest) (s func (s *AuthStorage) TerminateSession(_ context.Context, userID, clientID string) error { return nil } -func (s *AuthStorage) GetSigningKey(_ context.Context, keyCh chan<- jose.SigningKey, _ chan<- error, _ <-chan time.Time) { +func (s *AuthStorage) GetSigningKey(_ context.Context, keyCh chan<- jose.SigningKey) { keyCh <- jose.SigningKey{Algorithm: jose.RS256, Key: s.key} } func (s *AuthStorage) GetKey(_ context.Context) (*rsa.PrivateKey, error) { return s.key, nil } -func (s *AuthStorage) SaveNewKeyPair(ctx context.Context) error { - return nil -} func (s *AuthStorage) GetKeySet(_ context.Context) (*jose.JSONWebKeySet, error) { pubkey := s.key.Public() return &jose.JSONWebKeySet{ diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index 9e4963a..ea962d5 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -187,15 +187,15 @@ func (mr *MockStorageMockRecorder) GetPrivateClaimsFromScopes(arg0, arg1, arg2, } // GetSigningKey mocks base method -func (m *MockStorage) GetSigningKey(arg0 context.Context, arg1 chan<- jose.SigningKey, arg2 chan<- error, arg3 <-chan time.Time) { +func (m *MockStorage) GetSigningKey(arg0 context.Context, arg1 chan<- jose.SigningKey) { m.ctrl.T.Helper() - m.ctrl.Call(m, "GetSigningKey", arg0, arg1, arg2, arg3) + m.ctrl.Call(m, "GetSigningKey", arg0, arg1) } // GetSigningKey indicates an expected call of GetSigningKey -func (mr *MockStorageMockRecorder) GetSigningKey(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { +func (mr *MockStorageMockRecorder) GetSigningKey(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSigningKey", reflect.TypeOf((*MockStorage)(nil).GetSigningKey), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSigningKey", reflect.TypeOf((*MockStorage)(nil).GetSigningKey), arg0, arg1) } // GetUserinfoFromScopes mocks base method @@ -256,20 +256,6 @@ func (mr *MockStorageMockRecorder) SaveAuthCode(arg0, arg1, arg2 interface{}) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAuthCode", reflect.TypeOf((*MockStorage)(nil).SaveAuthCode), arg0, arg1, arg2) } -// SaveNewKeyPair mocks base method -func (m *MockStorage) SaveNewKeyPair(arg0 context.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SaveNewKeyPair", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// SaveNewKeyPair indicates an expected call of SaveNewKeyPair -func (mr *MockStorageMockRecorder) SaveNewKeyPair(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveNewKeyPair", reflect.TypeOf((*MockStorage)(nil).SaveNewKeyPair), arg0) -} - // TerminateSession mocks base method func (m *MockStorage) TerminateSession(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() diff --git a/pkg/op/op.go b/pkg/op/op.go index bba7a14..1a93024 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -6,7 +6,6 @@ import ( "net/http" "time" - "github.com/caos/logging" "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/gorilla/schema" @@ -132,7 +131,7 @@ func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opO keyCh := make(chan jose.SigningKey) o.signer = NewSigner(ctx, storage, keyCh) - go EnsureKey(ctx, storage, keyCh, o.timer, o.retry) + go storage.GetSigningKey(ctx, keyCh) o.httpHandler = CreateRouter(o, o.interceptors...) @@ -282,36 +281,6 @@ func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSig return payload, err } -func EnsureKey(ctx context.Context, storage Storage, keyCh chan<- jose.SigningKey, timer <-chan time.Time, retry func(int) (bool, int)) { - count := 0 - timer = time.After(0) - errCh := make(chan error) - go storage.GetSigningKey(ctx, keyCh, errCh, timer) - for { - select { - case <-ctx.Done(): - return - case err := <-errCh: - if err == nil { - continue - } - _, ok := err.(StorageNotFoundError) - if ok { - err := storage.SaveNewKeyPair(ctx) - if err == nil { - continue - } - } - ok, count = retry(count) - if ok { - timer = time.After(0) - continue - } - logging.Log("OP-n6ynVE").WithError(err).Panic("error in key signer") - } - } -} - type Option func(o *openidProvider) error func WithCustomAuthEndpoint(endpoint Endpoint) Option { @@ -382,27 +351,6 @@ func WithHttpInterceptors(interceptors ...HttpInterceptor) Option { } } -func WithRetry(max int, sleep time.Duration) Option { - return func(o *openidProvider) error { - o.retry = func(count int) (bool, int) { - count++ - if count == max { - return false, count - } - time.Sleep(sleep) - return true, count - } - return nil - } -} - -func WithTimer(timer <-chan time.Time) Option { - return func(o *openidProvider) error { - o.timer = timer - return nil - } -} - func buildInterceptor(interceptors ...HttpInterceptor) func(http.HandlerFunc) http.Handler { return func(handlerFunc http.HandlerFunc) http.Handler { handler := handlerFuncToHandler(handlerFunc) diff --git a/pkg/op/signer.go b/pkg/op/signer.go index 76bb9c7..939fe13 100644 --- a/pkg/op/signer.go +++ b/pkg/op/signer.go @@ -34,6 +34,9 @@ func (s *tokenSigner) Health(_ context.Context) error { if s.signer == nil { return errors.New("no signer") } + if string(s.alg) == "" { + return errors.New("no signing algorithm") + } return nil } diff --git a/pkg/op/storage.go b/pkg/op/storage.go index eba5003..92c88e6 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -20,9 +20,8 @@ type AuthStorage interface { TerminateSession(context.Context, string, string) error - GetSigningKey(context.Context, chan<- jose.SigningKey, chan<- error, <-chan time.Time) + GetSigningKey(context.Context, chan<- jose.SigningKey) GetKeySet(context.Context) (*jose.JSONWebKeySet, error) - SaveNewKeyPair(context.Context) error } type OPStorage interface { diff --git a/pkg/utils/sign.go b/pkg/utils/sign.go index e1efe61..5ebac43 100644 --- a/pkg/utils/sign.go +++ b/pkg/utils/sign.go @@ -2,6 +2,7 @@ package utils import ( "encoding/json" + "errors" "gopkg.in/square/go-jose.v2" ) @@ -15,6 +16,9 @@ func Sign(object interface{}, signer jose.Signer) (string, error) { } func SignPayload(payload []byte, signer jose.Signer) (string, error) { + if signer == nil { + return "", errors.New("missing signer") + } result, err := signer.Sign(payload) if err != nil { return "", err From 3acc62e79eaf24010ea5056c0d9d14ae3f4cb3ec Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Tue, 20 Oct 2020 07:39:36 +0200 Subject: [PATCH 002/502] cleanup --- pkg/op/mock/storage.mock.impl.go | 23 ++++------------------- pkg/op/storage.go | 4 ---- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/pkg/op/mock/storage.mock.impl.go b/pkg/op/mock/storage.mock.impl.go index de9dee9..441d4a0 100644 --- a/pkg/op/mock/storage.mock.impl.go +++ b/pkg/op/mock/storage.mock.impl.go @@ -38,12 +38,6 @@ func NewMockStorageAny(t *testing.T) op.Storage { return m } -func NewMockStorageSigningKeyError(t *testing.T) op.Storage { - m := NewStorage(t) - ExpectSigningKeyError(m) - return m -} - func NewMockStorageSigningKeyInvalid(t *testing.T) op.Storage { m := NewStorage(t) ExpectSigningKeyInvalid(m) @@ -89,19 +83,10 @@ func ExpectValidClientID(s op.Storage) { }) } -func ExpectSigningKeyError(s op.Storage) { - mockS := s.(*MockStorage) - mockS.EXPECT().GetSigningKey(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(_ context.Context, keyCh chan<- jose.SigningKey, errCh chan<- error, _ <-chan bool) { - errCh <- errors.New("error") - }, - ) -} - func ExpectSigningKeyInvalid(s op.Storage) { mockS := s.(*MockStorage) - mockS.EXPECT().GetSigningKey(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(_ context.Context, keyCh chan<- jose.SigningKey, errCh chan<- error, _ <-chan bool) { + mockS.EXPECT().GetSigningKey(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, keyCh chan<- jose.SigningKey) { keyCh <- jose.SigningKey{} }, ) @@ -109,8 +94,8 @@ func ExpectSigningKeyInvalid(s op.Storage) { func ExpectSigningKey(s op.Storage) { mockS := s.(*MockStorage) - mockS.EXPECT().GetSigningKey(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(_ context.Context, keyCh chan<- jose.SigningKey, errCh chan<- error, _ <-chan bool) { + mockS.EXPECT().GetSigningKey(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, keyCh chan<- jose.SigningKey) { keyCh <- jose.SigningKey{Algorithm: jose.HS256, Key: []byte("key")} }, ) diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 92c88e6..ab2fda2 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -39,10 +39,6 @@ type Storage interface { Health(context.Context) error } -type StorageNotFoundError interface { - IsNotFound() -} - type AuthRequest interface { GetID() string GetACR() string From 939e1095150206763039510532b85f2d328adcb7 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Wed, 21 Oct 2020 11:15:57 +0200 Subject: [PATCH 003/502] chore(workflow): update github actions in release workflow (#65) * chore(workflow): update github actions in release workflow * chore(workflow): add github-actions to dependabot.yml * fix dependabot.yml --- .github/dependabot.yml | 4 ++++ .github/workflows/release.yml | 6 ++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3d24fe9..79ff704 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,3 +9,7 @@ updates: commit-message: prefix: chore include: scope +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: weekly \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 18f5c01..49016bb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Setup go - uses: actions/setup-go@v2-beta + uses: actions/setup-go@v2 with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov ./pkg/... @@ -25,9 +25,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Source checkout - uses: actions/checkout@v1 - with: - fetch-depth: 1 + uses: actions/checkout@v2 - name: Semantic Release uses: cycjimmy/semantic-release-action@v2 with: From deb33653d4a6150f4dc29668101a9ce83cf0feff Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 23 Oct 2020 15:59:44 +0200 Subject: [PATCH 004/502] fix: decode basic auth header components (clientID, clientSecret) --- pkg/op/tokenrequest.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pkg/op/tokenrequest.go b/pkg/op/tokenrequest.go index d414221..c3860ff 100644 --- a/pkg/op/tokenrequest.go +++ b/pkg/op/tokenrequest.go @@ -4,6 +4,7 @@ import ( "context" "errors" "net/http" + "net/url" "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" @@ -84,9 +85,14 @@ func ParseAccessTokenRequest(r *http.Request, decoder utils.Decoder) (*oidc.Acce } clientID, clientSecret, ok := r.BasicAuth() if ok { - tokenReq.ClientID = clientID - tokenReq.ClientSecret = clientSecret - + tokenReq.ClientID, err = url.QueryUnescape(clientID) + if err != nil { + return nil, ErrInvalidRequest("invalid basic auth header") + } + tokenReq.ClientSecret, err = url.QueryUnescape(clientSecret) + if err != nil { + return nil, ErrInvalidRequest("invalid basic auth header") + } } return tokenReq, nil } @@ -115,7 +121,7 @@ func AuthorizeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exc return authReq, client, err } if client.AuthMethod() == AuthMethodPost && !exchanger.AuthMethodPostSupported() { - return nil, nil, errors.New("basic not supported") + return nil, nil, errors.New("auth_method post not supported") } err = AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, exchanger.Storage()) if err != nil { From ab9cef7605ccc71a450e8f51f1959fcb91cc70aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2020 04:06:24 +0000 Subject: [PATCH 005/502] chore(deps): bump golang.org/x/text from 0.3.3 to 0.3.4 Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.3 to 0.3.4. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.3.3...v0.3.4) Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 4112c69..9c46198 100644 --- a/go.mod +++ b/go.mod @@ -15,9 +15,9 @@ require ( github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/sirupsen/logrus v1.7.0 github.com/stretchr/testify v1.6.1 - golang.org/x/net v0.0.0-20200904194848-62affa334b73 + golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 - golang.org/x/text v0.3.3 + golang.org/x/text v0.3.4 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/square/go-jose.v2 v2.5.1 ) diff --git a/go.sum b/go.sum index b2b0f45..6eedcb0 100644 --- a/go.sum +++ b/go.sum @@ -126,8 +126,6 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -141,8 +139,6 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -280,6 +276,8 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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= From 2370409a55fbfe17706eaa1af8b60bf535e43a31 Mon Sep 17 00:00:00 2001 From: Fabi <38692350+fgerschwiler@users.noreply.github.com> Date: Tue, 3 Nov 2020 08:07:02 +0100 Subject: [PATCH 006/502] fix: allow additional scopes (#69) * feat: allow additional scopes * fix: mocks and tests * fix: restrict additional scopes * fix: restrict additional scopes * fix: remove comments * fix: remove comments --- example/internal/mock/storage.go | 14 ++++-- pkg/op/authrequest.go | 2 +- pkg/op/client.go | 6 +-- pkg/op/mock/client.go | 2 +- pkg/op/mock/client.mock.go | 84 ++++++++++++++++---------------- pkg/op/mock/storage.mock.impl.go | 15 ++++-- pkg/op/token.go | 31 +++--------- 7 files changed, 75 insertions(+), 79 deletions(-) diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index 9671ec7..aee9802 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -284,10 +284,18 @@ func (c *ConfClient) AllowedScopes() []string { return nil } -func (c *ConfClient) AssertAdditionalIdTokenScopes() bool { - return false +func (c *ConfClient) RestrictAdditionalIdTokenScopes() func(scopes []string) []string { + return func(scopes []string) []string { + return scopes + } } -func (c *ConfClient) AssertAdditionalAccessTokenScopes() bool { +func (c *ConfClient) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { + return func(scopes []string) []string { + return scopes + } +} + +func (c *ConfClient) IsScopeAllowed(scope string) bool { return false } diff --git a/pkg/op/authrequest.go b/pkg/op/authrequest.go index 4d6118c..9e320f8 100644 --- a/pkg/op/authrequest.go +++ b/pkg/op/authrequest.go @@ -121,7 +121,7 @@ func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) { scope == oidc.ScopePhone || scope == oidc.ScopeAddress || scope == oidc.ScopeOfflineAccess) && - !utils.Contains(client.AllowedScopes(), scope) { + !client.IsScopeAllowed(scope) { scopes[i] = scopes[len(scopes)-1] scopes[len(scopes)-1] = "" scopes = scopes[:len(scopes)-1] diff --git a/pkg/op/client.go b/pkg/op/client.go index 790933e..ceca8b0 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -34,9 +34,9 @@ type Client interface { AccessTokenType() AccessTokenType IDTokenLifetime() time.Duration DevMode() bool - AllowedScopes() []string - AssertAdditionalIdTokenScopes() bool - AssertAdditionalAccessTokenScopes() bool + RestrictAdditionalIdTokenScopes() func(scopes []string) []string + RestrictAdditionalAccessTokenScopes() func(scopes []string) []string + IsScopeAllowed(scope string) bool } func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseType) bool { diff --git a/pkg/op/mock/client.go b/pkg/op/mock/client.go index 12c00cc..b7ac3e8 100644 --- a/pkg/op/mock/client.go +++ b/pkg/op/mock/client.go @@ -26,7 +26,7 @@ func NewClientExpectAny(t *testing.T, appType op.ApplicationType) op.Client { func(id string) string { return "login?id=" + id }) - m.EXPECT().AllowedScopes().AnyTimes().Return(nil) + m.EXPECT().IsScopeAllowed(gomock.Any()).AnyTimes().Return(false) return c } diff --git a/pkg/op/mock/client.mock.go b/pkg/op/mock/client.mock.go index 0780623..df80bd0 100644 --- a/pkg/op/mock/client.mock.go +++ b/pkg/op/mock/client.mock.go @@ -49,20 +49,6 @@ func (mr *MockClientMockRecorder) AccessTokenType() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccessTokenType", reflect.TypeOf((*MockClient)(nil).AccessTokenType)) } -// AllowedScopes mocks base method -func (m *MockClient) AllowedScopes() []string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AllowedScopes") - ret0, _ := ret[0].([]string) - return ret0 -} - -// AllowedScopes indicates an expected call of AllowedScopes -func (mr *MockClientMockRecorder) AllowedScopes() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllowedScopes", reflect.TypeOf((*MockClient)(nil).AllowedScopes)) -} - // ApplicationType mocks base method func (m *MockClient) ApplicationType() op.ApplicationType { m.ctrl.T.Helper() @@ -77,34 +63,6 @@ func (mr *MockClientMockRecorder) ApplicationType() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplicationType", reflect.TypeOf((*MockClient)(nil).ApplicationType)) } -// AssertAdditionalAccessTokenScopes mocks base method -func (m *MockClient) AssertAdditionalAccessTokenScopes() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AssertAdditionalAccessTokenScopes") - ret0, _ := ret[0].(bool) - return ret0 -} - -// AssertAdditionalAccessTokenScopes indicates an expected call of AssertAdditionalAccessTokenScopes -func (mr *MockClientMockRecorder) AssertAdditionalAccessTokenScopes() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssertAdditionalAccessTokenScopes", reflect.TypeOf((*MockClient)(nil).AssertAdditionalAccessTokenScopes)) -} - -// AssertAdditionalIdTokenScopes mocks base method -func (m *MockClient) AssertAdditionalIdTokenScopes() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AssertAdditionalIdTokenScopes") - ret0, _ := ret[0].(bool) - return ret0 -} - -// AssertAdditionalIdTokenScopes indicates an expected call of AssertAdditionalIdTokenScopes -func (mr *MockClientMockRecorder) AssertAdditionalIdTokenScopes() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssertAdditionalIdTokenScopes", reflect.TypeOf((*MockClient)(nil).AssertAdditionalIdTokenScopes)) -} - // AuthMethod mocks base method func (m *MockClient) AuthMethod() op.AuthMethod { m.ctrl.T.Helper() @@ -161,6 +119,20 @@ func (mr *MockClientMockRecorder) IDTokenLifetime() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenLifetime", reflect.TypeOf((*MockClient)(nil).IDTokenLifetime)) } +// IsScopeAllowed mocks base method +func (m *MockClient) IsScopeAllowed(arg0 string) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsScopeAllowed", arg0) + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsScopeAllowed indicates an expected call of IsScopeAllowed +func (mr *MockClientMockRecorder) IsScopeAllowed(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsScopeAllowed", reflect.TypeOf((*MockClient)(nil).IsScopeAllowed), arg0) +} + // LoginURL mocks base method func (m *MockClient) LoginURL(arg0 string) string { m.ctrl.T.Helper() @@ -216,3 +188,31 @@ func (mr *MockClientMockRecorder) ResponseTypes() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResponseTypes", reflect.TypeOf((*MockClient)(nil).ResponseTypes)) } + +// RestrictAdditionalAccessTokenScopes mocks base method +func (m *MockClient) RestrictAdditionalAccessTokenScopes() func([]string) []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RestrictAdditionalAccessTokenScopes") + ret0, _ := ret[0].(func([]string) []string) + return ret0 +} + +// RestrictAdditionalAccessTokenScopes indicates an expected call of RestrictAdditionalAccessTokenScopes +func (mr *MockClientMockRecorder) RestrictAdditionalAccessTokenScopes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestrictAdditionalAccessTokenScopes", reflect.TypeOf((*MockClient)(nil).RestrictAdditionalAccessTokenScopes)) +} + +// RestrictAdditionalIdTokenScopes mocks base method +func (m *MockClient) RestrictAdditionalIdTokenScopes() func([]string) []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RestrictAdditionalIdTokenScopes") + ret0, _ := ret[0].(func([]string) []string) + return ret0 +} + +// RestrictAdditionalIdTokenScopes indicates an expected call of RestrictAdditionalIdTokenScopes +func (mr *MockClientMockRecorder) RestrictAdditionalIdTokenScopes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestrictAdditionalIdTokenScopes", reflect.TypeOf((*MockClient)(nil).RestrictAdditionalIdTokenScopes)) +} diff --git a/pkg/op/mock/storage.mock.impl.go b/pkg/op/mock/storage.mock.impl.go index de9dee9..92d5ad7 100644 --- a/pkg/op/mock/storage.mock.impl.go +++ b/pkg/op/mock/storage.mock.impl.go @@ -171,9 +171,16 @@ func (c *ConfClient) DevMode() bool { func (c *ConfClient) AllowedScopes() []string { return nil } -func (c *ConfClient) AssertAdditionalIdTokenScopes() bool { - return false -} -func (c *ConfClient) AssertAdditionalAccessTokenScopes() bool { +func (c *ConfClient) RestrictAdditionalIdTokenScopes() func(scopes []string) []string { + return func(scopes []string) []string { + return scopes + } +} +func (c *ConfClient) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { + return func(scopes []string) []string { + return scopes + } +} +func (c *ConfClient) IsScopeAllowed(scope string) bool { return false } diff --git a/pkg/op/token.go b/pkg/op/token.go index 2d66ef5..4fd4c0a 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -31,7 +31,7 @@ func CreateTokenResponse(ctx context.Context, authReq AuthRequest, client Client return nil, err } } - idToken, err := CreateIDToken(ctx, creator.Issuer(), authReq, client.IDTokenLifetime(), accessToken, code, creator.Storage(), creator.Signer(), client.AssertAdditionalIdTokenScopes()) + idToken, err := CreateIDToken(ctx, creator.Issuer(), authReq, client.IDTokenLifetime(), accessToken, code, creator.Storage(), creator.Signer(), client.RestrictAdditionalIdTokenScopes()) if err != nil { return nil, err } @@ -84,8 +84,9 @@ func CreateBearerToken(tokenID, subject string, crypto Crypto) (string, error) { func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, exp time.Time, id string, signer Signer, client Client, storage Storage) (string, error) { claims := oidc.NewAccessTokenClaims(issuer, tokenRequest.GetSubject(), tokenRequest.GetAudience(), exp, id) - if client != nil && client.AssertAdditionalAccessTokenScopes() { - privateClaims, err := storage.GetPrivateClaimsFromScopes(ctx, tokenRequest.GetSubject(), client.GetID(), removeUserinfoScopes(tokenRequest.GetScopes())) + if client != nil { + restrictedScopes := client.RestrictAdditionalAccessTokenScopes()(tokenRequest.GetScopes()) + privateClaims, err := storage.GetPrivateClaimsFromScopes(ctx, tokenRequest.GetSubject(), client.GetID(), removeUserinfoScopes(restrictedScopes)) if err != nil { return "", err } @@ -94,11 +95,10 @@ func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, ex return utils.Sign(claims, signer.Signer()) } -func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, validity time.Duration, accessToken, code string, storage Storage, signer Signer, additonalScopes bool) (string, error) { +func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, validity time.Duration, accessToken, code string, storage Storage, signer Signer, restictAdditionalScopesFunc func([]string) []string) (string, error) { exp := time.Now().UTC().Add(validity) claims := oidc.NewIDTokenClaims(issuer, authReq.GetSubject(), authReq.GetAudience(), exp, authReq.GetAuthTime(), authReq.GetNonce(), authReq.GetACR(), authReq.GetAMR(), authReq.GetClientID()) - scopes := authReq.GetScopes() - + scopes := restictAdditionalScopesFunc(authReq.GetScopes()) if accessToken != "" { atHash, err := oidc.ClaimHash(accessToken, signer.SignatureAlgorithm()) if err != nil { @@ -107,9 +107,6 @@ func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, vali claims.SetAccessTokenHash(atHash) scopes = removeUserinfoScopes(scopes) } - if !additonalScopes { - scopes = removeAdditionalScopes(scopes) - } if len(scopes) > 0 { userInfo, err := storage.GetUserinfoFromScopes(ctx, authReq.GetSubject(), authReq.GetClientID(), scopes) if err != nil { @@ -142,19 +139,3 @@ func removeUserinfoScopes(scopes []string) []string { } return scopes } - -func removeAdditionalScopes(scopes []string) []string { - for i := len(scopes) - 1; i >= 0; i-- { - if !(scopes[i] == oidc.ScopeOpenID || - scopes[i] == oidc.ScopeProfile || - scopes[i] == oidc.ScopeEmail || - scopes[i] == oidc.ScopeAddress || - scopes[i] == oidc.ScopePhone) { - - scopes[i] = scopes[len(scopes)-1] - scopes[len(scopes)-1] = "" - scopes = scopes[:len(scopes)-1] - } - } - return scopes -} From 13b14734b91deeeaeca058dd531141c99f8195d2 Mon Sep 17 00:00:00 2001 From: Fabi <38692350+fgerschwiler@users.noreply.github.com> Date: Mon, 16 Nov 2020 08:26:19 +0100 Subject: [PATCH 007/502] fix: append client id to aud (#71) * fix: append client id to aud * fix: append client id to aud * Update pkg/oidc/token.go Co-authored-by: Livio Amstutz Co-authored-by: Livio Amstutz --- pkg/oidc/token.go | 15 ++++++++++++++- pkg/op/token.go | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index 99f18c7..bd84e64 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -48,8 +48,11 @@ func EmptyAccessTokenClaims() AccessTokenClaims { return new(accessTokenClaims) } -func NewAccessTokenClaims(issuer, subject string, audience []string, expiration time.Time, id string) AccessTokenClaims { +func NewAccessTokenClaims(issuer, subject string, audience []string, expiration time.Time, id, clientID string) AccessTokenClaims { now := time.Now().UTC() + if len(audience) == 0 { + audience = append(audience, clientID) + } return &accessTokenClaims{ Issuer: issuer, Subject: subject, @@ -201,6 +204,7 @@ func EmptyIDTokenClaims() IDTokenClaims { } func NewIDTokenClaims(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string) IDTokenClaims { + audience = AppendClientIDToAudience(clientID, audience) return &idTokenClaims{ Issuer: issuer, Audience: audience, @@ -441,3 +445,12 @@ func ClaimHash(claim string, sigAlgorithm jose.SignatureAlgorithm) (string, erro return utils.HashString(hash, claim, true), nil } + +func AppendClientIDToAudience(clientID string, audience []string) []string { + for _, aud := range audience { + if aud == clientID { + return audience + } + } + return append(audience, clientID) +} diff --git a/pkg/op/token.go b/pkg/op/token.go index 4fd4c0a..057ab5b 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -83,7 +83,7 @@ func CreateBearerToken(tokenID, subject string, crypto Crypto) (string, error) { } func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, exp time.Time, id string, signer Signer, client Client, storage Storage) (string, error) { - claims := oidc.NewAccessTokenClaims(issuer, tokenRequest.GetSubject(), tokenRequest.GetAudience(), exp, id) + claims := oidc.NewAccessTokenClaims(issuer, tokenRequest.GetSubject(), tokenRequest.GetAudience(), exp, id, client.GetID()) if client != nil { restrictedScopes := client.RestrictAdditionalAccessTokenScopes()(tokenRequest.GetScopes()) privateClaims, err := storage.GetPrivateClaimsFromScopes(ctx, tokenRequest.GetSubject(), client.GetID(), removeUserinfoScopes(restrictedScopes)) From f5d0e64ff1e1b7dbbdbe844bc8113021aaf215bc Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Tue, 24 Nov 2020 15:56:12 +0100 Subject: [PATCH 008/502] chore(example): dynamic scopes in example (#72) --- example/client/app/app.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example/client/app/app.go b/example/client/app/app.go index 1c9c469..ad00adb 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "net/http" "os" + "strings" "time" "github.com/google/uuid" @@ -28,11 +29,11 @@ func main() { clientSecret := os.Getenv("CLIENT_SECRET") issuer := os.Getenv("ISSUER") port := os.Getenv("PORT") + scopes := strings.Split(os.Getenv("SCOPES"), " ") ctx := context.Background() redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath) - scopes := []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopeAddress} cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) provider, err := rp.NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, rp.WithPKCE(cookieHandler), From 24120554e57c31f924e40b8617cf0c1d360c2fe6 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Thu, 26 Nov 2020 15:41:53 +0100 Subject: [PATCH 009/502] feat: add clock skew and option to put userinfo (profile, email, phone, address) into id_token --- example/internal/mock/storage.go | 8 ++++++++ pkg/oidc/token.go | 10 +++++----- pkg/op/client.go | 2 ++ pkg/op/mock/client.mock.go | 28 ++++++++++++++++++++++++++++ pkg/op/mock/storage.mock.impl.go | 8 ++++++++ pkg/op/token.go | 18 ++++++++++-------- 6 files changed, 61 insertions(+), 13 deletions(-) diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index aee9802..497c1b0 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -299,3 +299,11 @@ func (c *ConfClient) RestrictAdditionalAccessTokenScopes() func(scopes []string) func (c *ConfClient) IsScopeAllowed(scope string) bool { return false } + +func (c *ConfClient) UserInfoInIDToken() bool { + return false +} + +func (c *ConfClient) ClockSkew() time.Duration { + return 0 +} diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index bd84e64..ff8f33e 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -48,8 +48,8 @@ func EmptyAccessTokenClaims() AccessTokenClaims { return new(accessTokenClaims) } -func NewAccessTokenClaims(issuer, subject string, audience []string, expiration time.Time, id, clientID string) AccessTokenClaims { - now := time.Now().UTC() +func NewAccessTokenClaims(issuer, subject string, audience []string, expiration time.Time, id, clientID string, skew time.Duration) AccessTokenClaims { + now := time.Now().UTC().Add(-skew) if len(audience) == 0 { audience = append(audience, clientID) } @@ -203,14 +203,14 @@ func EmptyIDTokenClaims() IDTokenClaims { return new(idTokenClaims) } -func NewIDTokenClaims(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string) IDTokenClaims { +func NewIDTokenClaims(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string, skew time.Duration) IDTokenClaims { audience = AppendClientIDToAudience(clientID, audience) return &idTokenClaims{ Issuer: issuer, Audience: audience, Expiration: Time(expiration), - IssuedAt: Time(time.Now().UTC()), - AuthTime: Time(authTime), + IssuedAt: Time(time.Now().UTC().Add(-skew)), + AuthTime: Time(authTime.Add(-skew)), Nonce: nonce, AuthenticationContextClassReference: acr, AuthenticationMethodsReferences: amr, diff --git a/pkg/op/client.go b/pkg/op/client.go index ceca8b0..06fbcc3 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -37,6 +37,8 @@ type Client interface { RestrictAdditionalIdTokenScopes() func(scopes []string) []string RestrictAdditionalAccessTokenScopes() func(scopes []string) []string IsScopeAllowed(scope string) bool + UserInfoInIDToken() bool + ClockSkew() time.Duration } func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseType) bool { diff --git a/pkg/op/mock/client.mock.go b/pkg/op/mock/client.mock.go index df80bd0..5a89c34 100644 --- a/pkg/op/mock/client.mock.go +++ b/pkg/op/mock/client.mock.go @@ -77,6 +77,20 @@ func (mr *MockClientMockRecorder) AuthMethod() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthMethod", reflect.TypeOf((*MockClient)(nil).AuthMethod)) } +// ClockSkew mocks base method +func (m *MockClient) ClockSkew() time.Duration { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ClockSkew") + ret0, _ := ret[0].(time.Duration) + return ret0 +} + +// ClockSkew indicates an expected call of ClockSkew +func (mr *MockClientMockRecorder) ClockSkew() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClockSkew", reflect.TypeOf((*MockClient)(nil).ClockSkew)) +} + // DevMode mocks base method func (m *MockClient) DevMode() bool { m.ctrl.T.Helper() @@ -216,3 +230,17 @@ func (mr *MockClientMockRecorder) RestrictAdditionalIdTokenScopes() *gomock.Call mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestrictAdditionalIdTokenScopes", reflect.TypeOf((*MockClient)(nil).RestrictAdditionalIdTokenScopes)) } + +// UserInfoInIDToken mocks base method +func (m *MockClient) UserInfoInIDToken() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UserInfoInIDToken") + ret0, _ := ret[0].(bool) + return ret0 +} + +// UserInfoInIDToken indicates an expected call of UserInfoInIDToken +func (mr *MockClientMockRecorder) UserInfoInIDToken() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserInfoInIDToken", reflect.TypeOf((*MockClient)(nil).UserInfoInIDToken)) +} diff --git a/pkg/op/mock/storage.mock.impl.go b/pkg/op/mock/storage.mock.impl.go index 92d5ad7..3accdef 100644 --- a/pkg/op/mock/storage.mock.impl.go +++ b/pkg/op/mock/storage.mock.impl.go @@ -184,3 +184,11 @@ func (c *ConfClient) RestrictAdditionalAccessTokenScopes() func(scopes []string) func (c *ConfClient) IsScopeAllowed(scope string) bool { return false } + +func (c *ConfClient) UserInfoInIDToken() bool { + return false +} + +func (c *ConfClient) ClockSkew() time.Duration { + return 0 +} diff --git a/pkg/op/token.go b/pkg/op/token.go index 057ab5b..cc6e11b 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -31,7 +31,7 @@ func CreateTokenResponse(ctx context.Context, authReq AuthRequest, client Client return nil, err } } - idToken, err := CreateIDToken(ctx, creator.Issuer(), authReq, client.IDTokenLifetime(), accessToken, code, creator.Storage(), creator.Signer(), client.RestrictAdditionalIdTokenScopes()) + idToken, err := CreateIDToken(ctx, creator.Issuer(), authReq, client.IDTokenLifetime(), accessToken, code, creator.Storage(), creator.Signer(), client) if err != nil { return nil, err } @@ -69,7 +69,7 @@ func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTok if err != nil { return "", 0, err } - validity = exp.Sub(time.Now().UTC()) + validity = exp.Add(client.ClockSkew()).Sub(time.Now().UTC()) if accessTokenType == AccessTokenTypeJWT { token, err = CreateJWT(ctx, creator.Issuer(), tokenRequest, exp, id, creator.Signer(), client, creator.Storage()) return @@ -83,7 +83,7 @@ func CreateBearerToken(tokenID, subject string, crypto Crypto) (string, error) { } func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, exp time.Time, id string, signer Signer, client Client, storage Storage) (string, error) { - claims := oidc.NewAccessTokenClaims(issuer, tokenRequest.GetSubject(), tokenRequest.GetAudience(), exp, id, client.GetID()) + claims := oidc.NewAccessTokenClaims(issuer, tokenRequest.GetSubject(), tokenRequest.GetAudience(), exp, id, client.GetID(), client.ClockSkew()) if client != nil { restrictedScopes := client.RestrictAdditionalAccessTokenScopes()(tokenRequest.GetScopes()) privateClaims, err := storage.GetPrivateClaimsFromScopes(ctx, tokenRequest.GetSubject(), client.GetID(), removeUserinfoScopes(restrictedScopes)) @@ -95,17 +95,19 @@ func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, ex return utils.Sign(claims, signer.Signer()) } -func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, validity time.Duration, accessToken, code string, storage Storage, signer Signer, restictAdditionalScopesFunc func([]string) []string) (string, error) { - exp := time.Now().UTC().Add(validity) - claims := oidc.NewIDTokenClaims(issuer, authReq.GetSubject(), authReq.GetAudience(), exp, authReq.GetAuthTime(), authReq.GetNonce(), authReq.GetACR(), authReq.GetAMR(), authReq.GetClientID()) - scopes := restictAdditionalScopesFunc(authReq.GetScopes()) +func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, validity time.Duration, accessToken, code string, storage Storage, signer Signer, client Client) (string, error) { + exp := time.Now().UTC().Add(client.ClockSkew()).Add(validity) + claims := oidc.NewIDTokenClaims(issuer, authReq.GetSubject(), authReq.GetAudience(), exp, authReq.GetAuthTime(), authReq.GetNonce(), authReq.GetACR(), authReq.GetAMR(), authReq.GetClientID(), client.ClockSkew()) + scopes := client.RestrictAdditionalIdTokenScopes()(authReq.GetScopes()) if accessToken != "" { atHash, err := oidc.ClaimHash(accessToken, signer.SignatureAlgorithm()) if err != nil { return "", err } claims.SetAccessTokenHash(atHash) - scopes = removeUserinfoScopes(scopes) + if !client.UserInfoInIDToken() { + scopes = removeUserinfoScopes(scopes) + } } if len(scopes) > 0 { userInfo, err := storage.GetUserinfoFromScopes(ctx, authReq.GetSubject(), authReq.GetClientID(), scopes) From 36800145d67eabd72780e45b45242c5a5509d346 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Thu, 26 Nov 2020 16:12:27 +0100 Subject: [PATCH 010/502] renaming --- example/internal/mock/storage.go | 2 +- pkg/op/client.go | 2 +- pkg/op/mock/client.mock.go | 28 ++++++++++++++-------------- pkg/op/mock/storage.mock.impl.go | 2 +- pkg/op/token.go | 2 +- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index 497c1b0..8c1ab38 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -300,7 +300,7 @@ func (c *ConfClient) IsScopeAllowed(scope string) bool { return false } -func (c *ConfClient) UserInfoInIDToken() bool { +func (c *ConfClient) IDTokenUserinfoClaimsAssertion() bool { return false } diff --git a/pkg/op/client.go b/pkg/op/client.go index 06fbcc3..6d0891c 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -37,7 +37,7 @@ type Client interface { RestrictAdditionalIdTokenScopes() func(scopes []string) []string RestrictAdditionalAccessTokenScopes() func(scopes []string) []string IsScopeAllowed(scope string) bool - UserInfoInIDToken() bool + IDTokenUserinfoClaimsAssertion() bool ClockSkew() time.Duration } diff --git a/pkg/op/mock/client.mock.go b/pkg/op/mock/client.mock.go index 5a89c34..1a15624 100644 --- a/pkg/op/mock/client.mock.go +++ b/pkg/op/mock/client.mock.go @@ -133,6 +133,20 @@ func (mr *MockClientMockRecorder) IDTokenLifetime() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenLifetime", reflect.TypeOf((*MockClient)(nil).IDTokenLifetime)) } +// IDTokenUserinfoClaimsAssertion mocks base method +func (m *MockClient) IDTokenUserinfoClaimsAssertion() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IDTokenUserinfoClaimsAssertion") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IDTokenUserinfoClaimsAssertion indicates an expected call of IDTokenUserinfoClaimsAssertion +func (mr *MockClientMockRecorder) IDTokenUserinfoClaimsAssertion() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenUserinfoClaimsAssertion", reflect.TypeOf((*MockClient)(nil).IDTokenUserinfoClaimsAssertion)) +} + // IsScopeAllowed mocks base method func (m *MockClient) IsScopeAllowed(arg0 string) bool { m.ctrl.T.Helper() @@ -230,17 +244,3 @@ func (mr *MockClientMockRecorder) RestrictAdditionalIdTokenScopes() *gomock.Call mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestrictAdditionalIdTokenScopes", reflect.TypeOf((*MockClient)(nil).RestrictAdditionalIdTokenScopes)) } - -// UserInfoInIDToken mocks base method -func (m *MockClient) UserInfoInIDToken() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UserInfoInIDToken") - ret0, _ := ret[0].(bool) - return ret0 -} - -// UserInfoInIDToken indicates an expected call of UserInfoInIDToken -func (mr *MockClientMockRecorder) UserInfoInIDToken() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserInfoInIDToken", reflect.TypeOf((*MockClient)(nil).UserInfoInIDToken)) -} diff --git a/pkg/op/mock/storage.mock.impl.go b/pkg/op/mock/storage.mock.impl.go index 3accdef..29d0d15 100644 --- a/pkg/op/mock/storage.mock.impl.go +++ b/pkg/op/mock/storage.mock.impl.go @@ -185,7 +185,7 @@ func (c *ConfClient) IsScopeAllowed(scope string) bool { return false } -func (c *ConfClient) UserInfoInIDToken() bool { +func (c *ConfClient) IDTokenUserinfoClaimsAssertion() bool { return false } diff --git a/pkg/op/token.go b/pkg/op/token.go index cc6e11b..fe6658a 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -105,7 +105,7 @@ func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, vali return "", err } claims.SetAccessTokenHash(atHash) - if !client.UserInfoInIDToken() { + if !client.IDTokenUserinfoClaimsAssertion() { scopes = removeUserinfoScopes(scopes) } } From 27f3bc0f4a5e34c92ab650f67757604139d1630b Mon Sep 17 00:00:00 2001 From: Fabi <38692350+fgerschwiler@users.noreply.github.com> Date: Mon, 30 Nov 2020 11:21:09 +0100 Subject: [PATCH 011/502] fix: change callbackpath (#74) * fix: append client id to aud * handle new callback path Co-authored-by: Livio Amstutz --- example/server/default/default.go | 2 +- pkg/op/op.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/server/default/default.go b/example/server/default/default.go index d5922d4..7edaf2e 100644 --- a/example/server/default/default.go +++ b/example/server/default/default.go @@ -68,5 +68,5 @@ func HandleLogin(w http.ResponseWriter, r *http.Request) { func HandleCallback(w http.ResponseWriter, r *http.Request) { r.ParseForm() client := r.FormValue("client") - http.Redirect(w, r, "/authorize/"+client, http.StatusFound) + http.Redirect(w, r, "/authorize/callback?id="+client, http.StatusFound) } diff --git a/pkg/op/op.go b/pkg/op/op.go index 3d2fe41..d16848e 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -76,7 +76,7 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router router.HandleFunc(readinessEndpoint, readyHandler(o.Probes())) router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Signer())) router.Handle(o.AuthorizationEndpoint().Relative(), intercept(authorizeHandler(o))) - router.Handle(o.AuthorizationEndpoint().Relative()+"/{id}", intercept(authorizeCallbackHandler(o))) + router.NewRoute().Path(o.AuthorizationEndpoint().Relative()+"/callback").Queries("id", "{id}").Handler(intercept(authorizeCallbackHandler(o))) router.Handle(o.TokenEndpoint().Relative(), intercept(tokenHandler(o))) router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o)) router.Handle(o.EndSessionEndpoint().Relative(), intercept(endSessionHandler(o))) From b23f37f7ebf7a5849666ce245366457324f7b818 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 21 Dec 2020 21:04:07 +0100 Subject: [PATCH 012/502] fix: clock skew when using jwt profile --- pkg/op/token.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/op/token.go b/pkg/op/token.go index fe6658a..5331d44 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -69,7 +69,11 @@ func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTok if err != nil { return "", 0, err } - validity = exp.Add(client.ClockSkew()).Sub(time.Now().UTC()) + var clockSkew time.Duration + if client != nil { + clockSkew = client.ClockSkew() + } + validity = exp.Add(clockSkew).Sub(time.Now().UTC()) if accessTokenType == AccessTokenTypeJWT { token, err = CreateJWT(ctx, creator.Issuer(), tokenRequest, exp, id, creator.Signer(), client, creator.Storage()) return From a1a21f0d596f105e67e6f4a212d07f2cdc144e00 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 8 Jan 2021 15:01:23 +0100 Subject: [PATCH 013/502] introspect --- pkg/oidc/introspection.go | 256 +++++++++++++++++++++++++++++++++++ pkg/op/config.go | 1 + pkg/op/discovery.go | 6 +- pkg/op/op.go | 17 ++- pkg/op/storage.go | 9 +- pkg/op/token_intospection.go | 58 ++++++++ 6 files changed, 341 insertions(+), 6 deletions(-) create mode 100644 pkg/oidc/introspection.go create mode 100644 pkg/op/token_intospection.go diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go new file mode 100644 index 0000000..7fa13de --- /dev/null +++ b/pkg/oidc/introspection.go @@ -0,0 +1,256 @@ +package oidc + +import ( + "encoding/json" + "fmt" + "time" + + "golang.org/x/text/language" + + "github.com/caos/oidc/pkg/utils" +) + +type IntrospectionRequest struct { + Token string `schema:"token"` +} + +type IntrospectionResponse interface { + UserInfoSetter + SetActive(bool) + IsActive() bool +} + +func NewIntrospectionResponse() IntrospectionResponse { + return &introspectionResponse{} +} + +type introspectionResponse struct { + Active bool `json:"active"` + Subject string `json:"sub,omitempty"` + userInfoProfile + userInfoEmail + userInfoPhone + Address UserInfoAddress `json:"address,omitempty"` + + claims map[string]interface{} +} + +func (u *introspectionResponse) IsActive() bool { + return u.Active +} +func (u *introspectionResponse) GetSubject() string { + return u.Subject +} + +func (u *introspectionResponse) GetName() string { + return u.Name +} + +func (u *introspectionResponse) GetGivenName() string { + return u.GivenName +} + +func (u *introspectionResponse) GetFamilyName() string { + return u.FamilyName +} + +func (u *introspectionResponse) GetMiddleName() string { + return u.MiddleName +} + +func (u *introspectionResponse) GetNickname() string { + return u.Nickname +} + +func (u *introspectionResponse) GetProfile() string { + return u.Profile +} + +func (u *introspectionResponse) GetPicture() string { + return u.Picture +} + +func (u *introspectionResponse) GetWebsite() string { + return u.Website +} + +func (u *introspectionResponse) GetGender() Gender { + return u.Gender +} + +func (u *introspectionResponse) GetBirthdate() string { + return u.Birthdate +} + +func (u *introspectionResponse) GetZoneinfo() string { + return u.Zoneinfo +} + +func (u *introspectionResponse) GetLocale() language.Tag { + return u.Locale +} + +func (u *introspectionResponse) GetPreferredUsername() string { + return u.PreferredUsername +} + +func (u *introspectionResponse) GetEmail() string { + return u.Email +} + +func (u *introspectionResponse) IsEmailVerified() bool { + return u.EmailVerified +} + +func (u *introspectionResponse) GetPhoneNumber() string { + return u.PhoneNumber +} + +func (u *introspectionResponse) IsPhoneNumberVerified() bool { + return u.PhoneNumberVerified +} + +func (u *introspectionResponse) GetAddress() UserInfoAddress { + return u.Address +} + +func (u *introspectionResponse) GetClaim(key string) interface{} { + return u.claims[key] +} + +func (u *introspectionResponse) SetActive(active bool) { + u.Active = active +} + +func (u *introspectionResponse) SetSubject(sub string) { + u.Subject = sub +} + +func (u *introspectionResponse) SetName(name string) { + u.Name = name +} + +func (u *introspectionResponse) SetGivenName(name string) { + u.GivenName = name +} + +func (u *introspectionResponse) SetFamilyName(name string) { + u.FamilyName = name +} + +func (u *introspectionResponse) SetMiddleName(name string) { + u.MiddleName = name +} + +func (u *introspectionResponse) SetNickname(name string) { + u.Nickname = name +} + +func (u *introspectionResponse) SetUpdatedAt(date time.Time) { + u.UpdatedAt = Time(date) +} + +func (u *introspectionResponse) SetProfile(profile string) { + u.Profile = profile +} + +func (u *introspectionResponse) SetPicture(picture string) { + u.Picture = picture +} + +func (u *introspectionResponse) SetWebsite(website string) { + u.Website = website +} + +func (u *introspectionResponse) SetGender(gender Gender) { + u.Gender = gender +} + +func (u *introspectionResponse) SetBirthdate(birthdate string) { + u.Birthdate = birthdate +} + +func (u *introspectionResponse) SetZoneinfo(zoneInfo string) { + u.Zoneinfo = zoneInfo +} + +func (u *introspectionResponse) SetLocale(locale language.Tag) { + u.Locale = locale +} + +func (u *introspectionResponse) SetPreferredUsername(name string) { + u.PreferredUsername = name +} + +func (u *introspectionResponse) SetEmail(email string, verified bool) { + u.Email = email + u.EmailVerified = verified +} + +func (u *introspectionResponse) SetPhone(phone string, verified bool) { + u.PhoneNumber = phone + u.PhoneNumberVerified = verified +} + +func (u *introspectionResponse) SetAddress(address UserInfoAddress) { + u.Address = address +} + +func (u *introspectionResponse) AppendClaims(key string, value interface{}) { + if u.claims == nil { + u.claims = make(map[string]interface{}) + } + u.claims[key] = value +} + +func (i *introspectionResponse) MarshalJSON() ([]byte, error) { + type Alias introspectionResponse + a := &struct { + *Alias + Locale interface{} `json:"locale,omitempty"` + UpdatedAt int64 `json:"updated_at,omitempty"` + PreferredUsername string `json:"username,omitempty"` + }{ + Alias: (*Alias)(i), + } + if !i.Locale.IsRoot() { + a.Locale = i.Locale + } + if !time.Time(i.UpdatedAt).IsZero() { + a.UpdatedAt = time.Time(i.UpdatedAt).Unix() + } + a.PreferredUsername = i.PreferredUsername + i.PreferredUsername = "" + + b, err := json.Marshal(a) + if err != nil { + return nil, err + } + + if len(i.claims) == 0 { + return b, nil + } + + claims, err := json.Marshal(i.claims) + if err != nil { + return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims) + } + return utils.ConcatenateJSON(b, claims) +} + +func (i *introspectionResponse) UnmarshalJSON(data []byte) error { + type Alias introspectionResponse + a := &struct { + *Alias + UpdatedAt int64 `json:"update_at,omitempty"` + }{ + Alias: (*Alias)(i), + } + if err := json.Unmarshal(data, &a); err != nil { + return err + } + + i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC()) + + return nil +} diff --git a/pkg/op/config.go b/pkg/op/config.go index a2b831e..f4fbb97 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -13,6 +13,7 @@ type Configuration interface { Issuer() string AuthorizationEndpoint() Endpoint TokenEndpoint() Endpoint + IntrospectionEndpoint() Endpoint UserinfoEndpoint() Endpoint EndSessionEndpoint() Endpoint KeysEndpoint() Endpoint diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 4bc1272..3bec79b 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -22,9 +22,9 @@ func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfigurati Issuer: c.Issuer(), AuthorizationEndpoint: c.AuthorizationEndpoint().Absolute(c.Issuer()), TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()), - // IntrospectionEndpoint: c.Intro().Absolute(c.Issuer()), - UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()), - EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), + IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()), + UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()), + EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), // CheckSessionIframe: c.TokenEndpoint().Absolute(c.Issuer())(c.CheckSessionIframe), JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), ScopesSupported: Scopes(c), diff --git a/pkg/op/op.go b/pkg/op/op.go index d16848e..76d5fcc 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -21,7 +21,7 @@ const ( readinessEndpoint = "/ready" defaultAuthorizationEndpoint = "authorize" defaulTokenEndpoint = "oauth/token" - defaultIntrospectEndpoint = "introspect" + defaultIntrospectEndpoint = "oauth/introspect" defaultUserinfoEndpoint = "userinfo" defaultEndSessionEndpoint = "end_session" defaultKeysEndpoint = "keys" @@ -78,6 +78,7 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router router.Handle(o.AuthorizationEndpoint().Relative(), intercept(authorizeHandler(o))) router.NewRoute().Path(o.AuthorizationEndpoint().Relative()+"/callback").Queries("id", "{id}").Handler(intercept(authorizeCallbackHandler(o))) router.Handle(o.TokenEndpoint().Relative(), intercept(tokenHandler(o))) + router.HandleFunc(o.IntrospectionEndpoint().Relative(), introspectionHandler(o)) router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o)) router.Handle(o.EndSessionEndpoint().Relative(), intercept(endSessionHandler(o))) router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o)) @@ -166,6 +167,10 @@ func (o *openidProvider) TokenEndpoint() Endpoint { return o.endpoints.Token } +func (o *openidProvider) IntrospectionEndpoint() Endpoint { + return o.endpoints.Introspection +} + func (o *openidProvider) UserinfoEndpoint() Endpoint { return o.endpoints.Userinfo } @@ -332,6 +337,16 @@ func WithCustomTokenEndpoint(endpoint Endpoint) Option { } } +func WithCustomIntrospectionEndpoint(endpoint Endpoint) Option { + return func(o *openidProvider) error { + if err := endpoint.Validate(); err != nil { + return err + } + o.endpoints.Introspection = endpoint + return nil + } +} + func WithCustomUserinfoEndpoint(endpoint Endpoint) Option { return func(o *openidProvider) error { if err := endpoint.Validate(); err != nil { diff --git a/pkg/op/storage.go b/pkg/op/storage.go index eba5003..0a0cec2 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -28,10 +28,15 @@ type AuthStorage interface { type OPStorage interface { GetClientByClientID(ctx context.Context, clientID string) (Client, error) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error - GetUserinfoFromScopes(ctx context.Context, userID, clientID string, scopes []string) (oidc.UserInfo, error) - GetUserinfoFromToken(ctx context.Context, tokenID, subject, origin string) (oidc.UserInfo, error) + SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error + SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) + + //deprecated: use GetUserinfoFromScopes instead + GetUserinfoFromScopes(ctx context.Context, userID, clientID string, scopes []string) (oidc.UserInfo, error) + //deprecated: use SetUserinfoFromToken instead + GetUserinfoFromToken(ctx context.Context, tokenID, subject, origin string) (oidc.UserInfo, error) } type Storage interface { diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go new file mode 100644 index 0000000..e6a5d4a --- /dev/null +++ b/pkg/op/token_intospection.go @@ -0,0 +1,58 @@ +package op + +import ( + "errors" + "net/http" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +type Introspector interface { + Decoder() utils.Decoder + Crypto() Crypto + Storage() Storage + AccessTokenVerifier() AccessTokenVerifier +} + +func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + Introspect(w, r, introspector) + } +} + +func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspector) { + //validate authorization + + response := oidc.NewIntrospectionResponse() + token, err := ParseTokenInrospectionRequest(r, introspector.Decoder()) + if err != nil { + utils.MarshalJSON(w, response) + return + } + tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, token) + if !ok { + utils.MarshalJSON(w, response) + return + } + err = introspector.Storage().SetUserinfoFromToken(r.Context(), response, tokenID, subject, r.Header.Get("origin")) + if err != nil { + utils.MarshalJSON(w, response) + return + } + response.SetActive(true) + utils.MarshalJSON(w, response) +} + +func ParseTokenInrospectionRequest(r *http.Request, decoder utils.Decoder) (string, error) { + err := r.ParseForm() + if err != nil { + return "", errors.New("unable to parse request") + } + req := new(oidc.IntrospectionRequest) + err = decoder.Decode(req, r.Form) + if err != nil { + return "", errors.New("unable to parse request") + } + return req.Token, nil +} From 50ab51bb4633fa1e2896f5376c4c3456213dd2b8 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Thu, 28 Jan 2021 08:41:36 +0100 Subject: [PATCH 014/502] introspect and client assertion --- example/internal/mock/storage.go | 12 ++++----- pkg/oidc/discovery.go | 43 +++++++++++++++++++------------ pkg/oidc/introspection.go | 9 +++---- pkg/oidc/token_request.go | 16 ++++++++---- pkg/op/client.go | 2 +- pkg/op/config.go | 1 + pkg/op/discovery.go | 12 ++++++--- pkg/op/mock/client.mock.go | 4 +-- pkg/op/mock/configuration.mock.go | 28 ++++++++++++++++++++ pkg/op/mock/storage.mock.go | 28 ++++++++++++++++++++ pkg/op/mock/storage.mock.impl.go | 12 ++++----- pkg/op/op.go | 12 ++++++--- pkg/op/storage.go | 1 + pkg/op/tokenrequest.go | 43 ++++++++++++++++++++++++++----- pkg/op/verifier_jwt_profile.go | 8 +++--- 15 files changed, 171 insertions(+), 60 deletions(-) diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index 8c1ab38..40a1f86 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -184,22 +184,22 @@ func (s *AuthStorage) GetClientByClientID(_ context.Context, id string) (op.Clie return nil, errors.New("not found") } var appType op.ApplicationType - var authMethod op.AuthMethod + var authMethod oidc.AuthMethod var accessTokenType op.AccessTokenType var responseTypes []oidc.ResponseType if id == "web" { appType = op.ApplicationTypeWeb - authMethod = op.AuthMethodBasic + authMethod = oidc.AuthMethodBasic accessTokenType = op.AccessTokenTypeBearer responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode} } else if id == "native" { appType = op.ApplicationTypeNative - authMethod = op.AuthMethodNone + authMethod = oidc.AuthMethodNone accessTokenType = op.AccessTokenTypeBearer responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode} } else { appType = op.ApplicationTypeUserAgent - authMethod = op.AuthMethodNone + authMethod = oidc.AuthMethodNone accessTokenType = op.AccessTokenTypeJWT responseTypes = []oidc.ResponseType{oidc.ResponseTypeIDToken, oidc.ResponseTypeIDTokenOnly} } @@ -229,7 +229,7 @@ func (s *AuthStorage) GetPrivateClaimsFromScopes(_ context.Context, _, _ string, type ConfClient struct { applicationType op.ApplicationType - authMethod op.AuthMethod + authMethod oidc.AuthMethod responseTypes []oidc.ResponseType ID string accessTokenType op.AccessTokenType @@ -262,7 +262,7 @@ func (c *ConfClient) ApplicationType() op.ApplicationType { return c.applicationType } -func (c *ConfClient) AuthMethod() op.AuthMethod { +func (c *ConfClient) AuthMethod() oidc.AuthMethod { return c.authMethod } diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go index 9333ca9..4621a1f 100644 --- a/pkg/oidc/discovery.go +++ b/pkg/oidc/discovery.go @@ -5,21 +5,30 @@ const ( ) type DiscoveryConfiguration struct { - Issuer string `json:"issuer,omitempty"` - AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` - TokenEndpoint string `json:"token_endpoint,omitempty"` - IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` - UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` - EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` - CheckSessionIframe string `json:"check_session_iframe,omitempty"` - JwksURI string `json:"jwks_uri,omitempty"` - ScopesSupported []string `json:"scopes_supported,omitempty"` - ResponseTypesSupported []string `json:"response_types_supported,omitempty"` - ResponseModesSupported []string `json:"response_modes_supported,omitempty"` - GrantTypesSupported []string `json:"grant_types_supported,omitempty"` - SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` - IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` - TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"` - CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported,omitempty"` - ClaimsSupported []string `json:"claims_supported,omitempty"` + Issuer string `json:"issuer,omitempty"` + AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` + TokenEndpoint string `json:"token_endpoint,omitempty"` + IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` + UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` + EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` + CheckSessionIframe string `json:"check_session_iframe,omitempty"` + JwksURI string `json:"jwks_uri,omitempty"` + ScopesSupported []string `json:"scopes_supported,omitempty"` + ResponseTypesSupported []string `json:"response_types_supported,omitempty"` + ResponseModesSupported []string `json:"response_modes_supported,omitempty"` + GrantTypesSupported []string `json:"grant_types_supported,omitempty"` + SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` + IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` + TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"` + CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported,omitempty"` + ClaimsSupported []string `json:"claims_supported,omitempty"` } + +type AuthMethod string + +const ( + AuthMethodBasic AuthMethod = "client_secret_basic" + AuthMethodPost AuthMethod = "client_secret_post" + AuthMethodNone AuthMethod = "none" + AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt" +) diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index 7fa13de..6414bef 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -207,9 +207,9 @@ func (i *introspectionResponse) MarshalJSON() ([]byte, error) { type Alias introspectionResponse a := &struct { *Alias - Locale interface{} `json:"locale,omitempty"` - UpdatedAt int64 `json:"updated_at,omitempty"` - PreferredUsername string `json:"username,omitempty"` + Locale interface{} `json:"locale,omitempty"` + UpdatedAt int64 `json:"updated_at,omitempty"` + Username string `json:"username,omitempty"` }{ Alias: (*Alias)(i), } @@ -219,8 +219,7 @@ func (i *introspectionResponse) MarshalJSON() ([]byte, error) { if !time.Time(i.UpdatedAt).IsZero() { a.UpdatedAt = time.Time(i.UpdatedAt).Unix() } - a.PreferredUsername = i.PreferredUsername - i.PreferredUsername = "" + a.Username = i.PreferredUsername b, err := json.Marshal(a) if err != nil { diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index 1312b18..0c5b70b 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -15,6 +15,10 @@ const ( //GrantTypeTokenExchange defines the grant_type `urn:ietf:params:oauth:grant-type:token-exchange` used for the OAuth Token Exchange Grant GrantTypeTokenExchange GrantType = "urn:ietf:params:oauth:grant-type:token-exchange" + + //ClientAssertionTypeJWTAssertion defines the client_assertion_type `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` + //used for the OAuth JWT Profile Client Authentication + ClientAssertionTypeJWTAssertion = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" ) type GrantType string @@ -27,11 +31,13 @@ type TokenRequest interface { type TokenRequestType GrantType type AccessTokenRequest struct { - Code string `schema:"code"` - RedirectURI string `schema:"redirect_uri"` - ClientID string `schema:"client_id"` - ClientSecret string `schema:"client_secret"` - CodeVerifier string `schema:"code_verifier"` + Code string `schema:"code"` + RedirectURI string `schema:"redirect_uri"` + ClientID string `schema:"client_id"` + ClientSecret string `schema:"client_secret"` + CodeVerifier string `schema:"code_verifier"` + ClientAssertion string `schema:"client_assertion"` + ClientAssertionType string `schema:"client_assertion_type"` } func (a *AccessTokenRequest) GrantType() GrantType { diff --git a/pkg/op/client.go b/pkg/op/client.go index 6d0891c..79715b0 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -28,7 +28,7 @@ type Client interface { RedirectURIs() []string PostLogoutRedirectURIs() []string ApplicationType() ApplicationType - AuthMethod() AuthMethod + AuthMethod() oidc.AuthMethod ResponseTypes() []oidc.ResponseType LoginURL(string) string AccessTokenType() AccessTokenType diff --git a/pkg/op/config.go b/pkg/op/config.go index f4fbb97..7cb522a 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -20,6 +20,7 @@ type Configuration interface { AuthMethodPostSupported() bool CodeMethodS256Supported() bool + AuthMethodPrivateKeyJWTSupported() bool GrantTypeTokenExchangeSupported() bool GrantTypeJWTAuthorizationSupported() bool } diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 3bec79b..8708fbb 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -108,12 +108,16 @@ func SubjectTypes(c Configuration) []string { return []string{"public"} //TODO: config } -func AuthMethods(c Configuration) []string { - authMethods := []string{ - string(AuthMethodBasic), +func AuthMethods(c Configuration) []oidc.AuthMethod { + authMethods := []oidc.AuthMethod{ + oidc.AuthMethodNone, + oidc.AuthMethodBasic, } if c.AuthMethodPostSupported() { - authMethods = append(authMethods, string(AuthMethodPost)) + authMethods = append(authMethods, oidc.AuthMethodPost) + } + if c.AuthMethodPrivateKeyJWTSupported() { + authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT) } return authMethods } diff --git a/pkg/op/mock/client.mock.go b/pkg/op/mock/client.mock.go index 1a15624..9d5fe41 100644 --- a/pkg/op/mock/client.mock.go +++ b/pkg/op/mock/client.mock.go @@ -64,10 +64,10 @@ func (mr *MockClientMockRecorder) ApplicationType() *gomock.Call { } // AuthMethod mocks base method -func (m *MockClient) AuthMethod() op.AuthMethod { +func (m *MockClient) AuthMethod() oidc.AuthMethod { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AuthMethod") - ret0, _ := ret[0].(op.AuthMethod) + ret0, _ := ret[0].(oidc.AuthMethod) return ret0 } diff --git a/pkg/op/mock/configuration.mock.go b/pkg/op/mock/configuration.mock.go index ece747c..4f83f35 100644 --- a/pkg/op/mock/configuration.mock.go +++ b/pkg/op/mock/configuration.mock.go @@ -47,6 +47,20 @@ func (mr *MockConfigurationMockRecorder) AuthMethodPostSupported() *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthMethodPostSupported", reflect.TypeOf((*MockConfiguration)(nil).AuthMethodPostSupported)) } +// AuthMethodPrivateKeyJWTSupported mocks base method +func (m *MockConfiguration) AuthMethodPrivateKeyJWTSupported() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AuthMethodPrivateKeyJWTSupported") + ret0, _ := ret[0].(bool) + return ret0 +} + +// AuthMethodPrivateKeyJWTSupported indicates an expected call of AuthMethodPrivateKeyJWTSupported +func (mr *MockConfigurationMockRecorder) AuthMethodPrivateKeyJWTSupported() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthMethodPrivateKeyJWTSupported", reflect.TypeOf((*MockConfiguration)(nil).AuthMethodPrivateKeyJWTSupported)) +} + // AuthorizationEndpoint mocks base method func (m *MockConfiguration) AuthorizationEndpoint() op.Endpoint { m.ctrl.T.Helper() @@ -117,6 +131,20 @@ func (mr *MockConfigurationMockRecorder) GrantTypeTokenExchangeSupported() *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeTokenExchangeSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeTokenExchangeSupported)) } +// IntrospectionEndpoint mocks base method +func (m *MockConfiguration) IntrospectionEndpoint() op.Endpoint { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IntrospectionEndpoint") + ret0, _ := ret[0].(op.Endpoint) + return ret0 +} + +// IntrospectionEndpoint indicates an expected call of IntrospectionEndpoint +func (mr *MockConfigurationMockRecorder) IntrospectionEndpoint() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntrospectionEndpoint", reflect.TypeOf((*MockConfiguration)(nil).IntrospectionEndpoint)) +} + // Issuer mocks base method func (m *MockConfiguration) Issuer() string { m.ctrl.T.Helper() diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index 9e4963a..b9adcec 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -270,6 +270,34 @@ func (mr *MockStorageMockRecorder) SaveNewKeyPair(arg0 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveNewKeyPair", reflect.TypeOf((*MockStorage)(nil).SaveNewKeyPair), arg0) } +// SetUserinfoFromScopes mocks base method +func (m *MockStorage) SetUserinfoFromScopes(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3 string, arg4 []string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetUserinfoFromScopes", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetUserinfoFromScopes indicates an expected call of SetUserinfoFromScopes +func (mr *MockStorageMockRecorder) SetUserinfoFromScopes(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserinfoFromScopes", reflect.TypeOf((*MockStorage)(nil).SetUserinfoFromScopes), arg0, arg1, arg2, arg3, arg4) +} + +// SetUserinfoFromToken mocks base method +func (m *MockStorage) SetUserinfoFromToken(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3, arg4 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetUserinfoFromToken", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetUserinfoFromToken indicates an expected call of SetUserinfoFromToken +func (mr *MockStorageMockRecorder) SetUserinfoFromToken(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserinfoFromToken", reflect.TypeOf((*MockStorage)(nil).SetUserinfoFromToken), arg0, arg1, arg2, arg3, arg4) +} + // TerminateSession mocks base method func (m *MockStorage) TerminateSession(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() diff --git a/pkg/op/mock/storage.mock.impl.go b/pkg/op/mock/storage.mock.impl.go index 29d0d15..2788c39 100644 --- a/pkg/op/mock/storage.mock.impl.go +++ b/pkg/op/mock/storage.mock.impl.go @@ -65,23 +65,23 @@ func ExpectValidClientID(s op.Storage) { mockS.EXPECT().GetClientByClientID(gomock.Any(), gomock.Any()).DoAndReturn( func(_ context.Context, id string) (op.Client, error) { var appType op.ApplicationType - var authMethod op.AuthMethod + var authMethod oidc.AuthMethod var accessTokenType op.AccessTokenType var responseTypes []oidc.ResponseType switch id { case "web_client": appType = op.ApplicationTypeWeb - authMethod = op.AuthMethodBasic + authMethod = oidc.AuthMethodBasic accessTokenType = op.AccessTokenTypeBearer responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode} case "native_client": appType = op.ApplicationTypeNative - authMethod = op.AuthMethodNone + authMethod = oidc.AuthMethodNone accessTokenType = op.AccessTokenTypeBearer responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode} case "useragent_client": appType = op.ApplicationTypeUserAgent - authMethod = op.AuthMethodBasic + authMethod = oidc.AuthMethodBasic accessTokenType = op.AccessTokenTypeJWT responseTypes = []oidc.ResponseType{oidc.ResponseTypeIDToken} } @@ -119,7 +119,7 @@ func ExpectSigningKey(s op.Storage) { type ConfClient struct { id string appType op.ApplicationType - authMethod op.AuthMethod + authMethod oidc.AuthMethod accessTokenType op.AccessTokenType responseTypes []oidc.ResponseType devMode bool @@ -145,7 +145,7 @@ func (c *ConfClient) ApplicationType() op.ApplicationType { return c.appType } -func (c *ConfClient) AuthMethod() op.AuthMethod { +func (c *ConfClient) AuthMethod() oidc.AuthMethod { return c.authMethod } diff --git a/pkg/op/op.go b/pkg/op/op.go index 76d5fcc..fa32a23 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -26,9 +26,10 @@ const ( defaultEndSessionEndpoint = "end_session" defaultKeysEndpoint = "keys" - AuthMethodBasic AuthMethod = "client_secret_basic" - AuthMethodPost AuthMethod = "client_secret_post" - AuthMethodNone AuthMethod = "none" + AuthMethodBasic AuthMethod = "client_secret_basic" + AuthMethodPost AuthMethod = "client_secret_post" + AuthMethodNone AuthMethod = "none" + AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt" CodeMethodS256 = "S256" ) @@ -90,6 +91,7 @@ type Config struct { CryptoKey [32]byte DefaultLogoutRedirectURI string CodeMethodS256 bool + AuthMethodPrivateKeyJWT bool } type endpoints struct { @@ -191,6 +193,10 @@ func (o *openidProvider) CodeMethodS256Supported() bool { return o.config.CodeMethodS256 } +func (o *openidProvider) AuthMethodPrivateKeyJWTSupported() bool { + return o.config.AuthMethodPrivateKeyJWT +} + func (o *openidProvider) GrantTypeTokenExchangeSupported() bool { return false } diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 0a0cec2..b5f1dfe 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -32,6 +32,7 @@ type OPStorage interface { SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) + //ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) //deprecated: use GetUserinfoFromScopes instead GetUserinfoFromScopes(ctx context.Context, userID, clientID string, scopes []string) (oidc.UserInfo, error) diff --git a/pkg/op/tokenrequest.go b/pkg/op/tokenrequest.go index c3860ff..0e295a3 100644 --- a/pkg/op/tokenrequest.go +++ b/pkg/op/tokenrequest.go @@ -18,6 +18,7 @@ type Exchanger interface { Signer() Signer Crypto() Crypto AuthMethodPostSupported() bool + AuthMethodPrivateKeyJWTSupported() bool GrantTypeTokenExchangeSupported() bool GrantTypeJWTAuthorizationSupported() bool } @@ -112,18 +113,30 @@ func ValidateAccessTokenRequest(ctx context.Context, tokenReq *oidc.AccessTokenR } func AuthorizeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (AuthRequest, Client, error) { + if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion { + jwtExchanger, ok := exchanger.(JWTAuthorizationGrantExchanger) + if !ok || !exchanger.AuthMethodPrivateKeyJWTSupported() { + return nil, nil, errors.New("auth_method private_key_jwt not supported") + } + return AuthorizePrivateJWTKey(ctx, tokenReq, jwtExchanger) + } client, err := exchanger.Storage().GetClientByClientID(ctx, tokenReq.ClientID) if err != nil { return nil, nil, err } - if client.AuthMethod() == AuthMethodNone { + if client.AuthMethod() == oidc.AuthMethodNone { authReq, err := AuthorizeCodeChallenge(ctx, tokenReq, exchanger) return authReq, client, err } - if client.AuthMethod() == AuthMethodPost && !exchanger.AuthMethodPostSupported() { + if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() { return nil, nil, errors.New("auth_method post not supported") } - err = AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, exchanger.Storage()) + authReq, err := AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, tokenReq.Code, exchanger.Storage()) + return authReq, client, err +} + +func AuthorizePrivateJWTKey(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger JWTAuthorizationGrantExchanger) (AuthRequest, Client, error) { + jwtReq, err := VerifyJWTAssertion(ctx, tokenReq.ClientAssertion, exchanger.JWTProfileVerifier()) if err != nil { return nil, nil, err } @@ -131,11 +144,26 @@ func AuthorizeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exc if err != nil { return nil, nil, ErrInvalidRequest("invalid code") } + client, err := exchanger.Storage().GetClientByClientID(ctx, jwtReq.Issuer) + if err != nil { + return nil, nil, err + } + if client.AuthMethod() != oidc.AuthMethodPrivateKeyJWT { + return nil, nil, ErrInvalidRequest("invalid_client") + } return authReq, client, nil } -func AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string, storage OPStorage) error { - return storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret) +func AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret, code string, storage Storage) (AuthRequest, error) { + err := storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret) + if err != nil { + return nil, err + } + authReq, err := storage.AuthRequestByCode(ctx, code) + if err != nil { + return nil, ErrInvalidRequest("invalid code") + } + return authReq, nil } func AuthorizeCodeChallenge(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (AuthRequest, error) { @@ -158,12 +186,15 @@ func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizati RequestError(w, r, err) } - tokenRequest, err := VerifyJWTAssertion(r.Context(), profileRequest, exchanger.JWTProfileVerifier()) + tokenRequest, err := VerifyJWTAssertion(r.Context(), profileRequest.Assertion, exchanger.JWTProfileVerifier()) if err != nil { RequestError(w, r, err) return } + //TODO: filter scopes + tokenRequest.Scopes = profileRequest.Scope + resp, err := CreateJWTTokenResponse(r.Context(), tokenRequest, exchanger) if err != nil { RequestError(w, r, err) diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index 8a31253..b30bdc5 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -8,7 +8,6 @@ import ( "gopkg.in/square/go-jose.v2" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" ) type JWTProfileVerifier interface { @@ -48,9 +47,9 @@ func (v *jwtProfileVerifier) Offset() time.Duration { return v.offset } -func VerifyJWTAssertion(ctx context.Context, profileRequest *tokenexchange.JWTProfileRequest, v JWTProfileVerifier) (*oidc.JWTTokenRequest, error) { +func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerifier) (*oidc.JWTTokenRequest, error) { request := new(oidc.JWTTokenRequest) - payload, err := oidc.ParseToken(profileRequest.Assertion, request) + payload, err := oidc.ParseToken(assertion, request) if err != nil { return nil, err } @@ -73,10 +72,9 @@ func VerifyJWTAssertion(ctx context.Context, profileRequest *tokenexchange.JWTPr keySet := &jwtProfileKeySet{v.Storage(), request.Subject} - if err = oidc.CheckSignature(ctx, profileRequest.Assertion, payload, request, nil, keySet); err != nil { + if err = oidc.CheckSignature(ctx, assertion, payload, request, nil, keySet); err != nil { return nil, err } - request.Scopes = profileRequest.Scope return request, nil } From f47821584ed748ad42a8f657af3fd94fdbc9b704 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Jan 2021 09:37:46 +0100 Subject: [PATCH 015/502] chore(deps): bump golang.org/x/text from 0.3.4 to 0.3.5 (#78) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.4 to 0.3.5. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.3.4...v0.3.5) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9c46198..8f7a95c 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/stretchr/testify v1.6.1 golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 - golang.org/x/text v0.3.4 + golang.org/x/text v0.3.5 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/square/go-jose.v2 v2.5.1 ) diff --git a/go.sum b/go.sum index 6eedcb0..3c72ca2 100644 --- a/go.sum +++ b/go.sum @@ -276,8 +276,8 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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= From 95cd01094a763728768004de5067052f5f639922 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Jan 2021 09:38:16 +0100 Subject: [PATCH 016/502] chore(deps): bump github.com/stretchr/testify from 1.6.1 to 1.7.0 (#79) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.6.1 to 1.7.0. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.6.1...v1.7.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8f7a95c..b062e57 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/gorilla/securecookie v1.1.1 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/sirupsen/logrus v1.7.0 - github.com/stretchr/testify v1.6.1 + github.com/stretchr/testify v1.7.0 golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 golang.org/x/text v0.3.5 diff --git a/go.sum b/go.sum index 3c72ca2..ee0c389 100644 --- a/go.sum +++ b/go.sum @@ -146,8 +146,8 @@ github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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= From ba01bdf1ef1eb6768ad2753cf4d795a45e7dff3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Jan 2021 09:39:01 +0100 Subject: [PATCH 017/502] chore(deps): bump github.com/google/uuid from 1.1.2 to 1.2.0 (#81) Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.1.2 to 1.2.0. - [Release notes](https://github.com/google/uuid/releases) - [Commits](https://github.com/google/uuid/compare/v1.1.2...v1.2.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b062e57..ad3cdea 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/golang/mock v1.4.4 github.com/google/go-cmp v0.5.2 // indirect github.com/google/go-github/v31 v31.0.0 - github.com/google/uuid v1.1.2 + github.com/google/uuid v1.2.0 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 github.com/gorilla/schema v1.2.0 diff --git a/go.sum b/go.sum index ee0c389..edb89be 100644 --- a/go.sum +++ b/go.sum @@ -105,8 +105,8 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= From 960be5af1f99630e08201087852e6c603d7e4637 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 1 Feb 2021 17:17:40 +0100 Subject: [PATCH 018/502] introspect and client assertion --- example/client/api/api.go | 166 ++++++++++-------- pkg/oidc/code_challenge.go | 2 +- pkg/oidc/discovery.go | 34 ++-- .../grants/tokenexchange/tokenexchange.go | 20 --- pkg/oidc/introspection.go | 4 + pkg/oidc/jwt_profile.go | 18 ++ pkg/oidc/token.go | 63 ++++++- pkg/op/authrequest.go | 18 ++ pkg/op/discovery.go | 6 +- pkg/op/discovery_test.go | 4 +- pkg/op/op.go | 18 +- pkg/op/probes.go | 2 +- pkg/op/storage.go | 17 -- pkg/op/tokenrequest.go | 5 +- pkg/op/verifier_jwt_profile.go | 2 +- pkg/rp/key.go | 33 ++++ pkg/rp/relaying_party.go | 33 +++- pkg/rp/resource_server.go | 118 +++++++++++++ pkg/rp/tockenexchange.go | 6 +- 19 files changed, 413 insertions(+), 156 deletions(-) create mode 100644 pkg/oidc/jwt_profile.go create mode 100644 pkg/rp/key.go create mode 100644 pkg/rp/resource_server.go diff --git a/example/client/api/api.go b/example/client/api/api.go index 6e1b0bd..e5345a8 100644 --- a/example/client/api/api.go +++ b/example/client/api/api.go @@ -1,90 +1,102 @@ package main -// import ( -// "encoding/json" -// "fmt" -// "log" -// "net/http" -// "os" +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "strings" + "time" -// "github.com/caos/oidc/pkg/oidc" -// "github.com/caos/oidc/pkg/oidc/rp" -// "github.com/caos/utils/logging" -// ) + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" -// const ( -// publicURL string = "/public" -// protectedURL string = "/protected" -// protectedExchangeURL string = "/protected/exchange" -// ) + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/rp" +) + +const ( + publicURL string = "/public" + protectedURL string = "/protected" + protectedClaimURL string = "/protected/{claim}/{value}" +) func main() { - // clientID := os.Getenv("CLIENT_ID") - // clientSecret := os.Getenv("CLIENT_SECRET") - // issuer := os.Getenv("ISSUER") - // port := os.Getenv("PORT") + keyPath := os.Getenv("KEY") + port := os.Getenv("PORT") - // // ctx := context.Background() + provider, err := rp.NewResourceServerFromKeyFile(keyPath) + if err != nil { + logrus.Fatalf("error creating provider %s", err.Error()) + } - // providerConfig := &oidc.ProviderConfig{ - // ClientID: clientID, - // ClientSecret: clientSecret, - // Issuer: issuer, - // } - // provider, err := rp.NewDefaultProvider(providerConfig) - // logging.Log("APP-nx6PeF").OnError(err).Panic("error creating provider") + router := mux.NewRouter() - // http.HandleFunc(publicURL, func(w http.ResponseWriter, r *http.Request) { - // w.Write([]byte("OK")) - // }) + //public url accessible without any authorization + //will print `OK` and current timestamp + router.HandleFunc(publicURL, func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("OK " + time.Now().String())) + }) - // http.HandleFunc(protectedURL, func(w http.ResponseWriter, r *http.Request) { - // ok, token := checkToken(w, r) - // if !ok { - // return - // } - // resp, err := provider.Introspect(r.Context(), token) - // if err != nil { - // http.Error(w, err.Error(), http.StatusForbidden) - // return - // } - // data, err := json.Marshal(resp) - // if err != nil { - // http.Error(w, err.Error(), http.StatusInternalServerError) - // return - // } - // w.Write(data) - // }) + //protected url which needs an active token + //will print the result of the introspection endpoint on success + router.HandleFunc(protectedURL, func(w http.ResponseWriter, r *http.Request) { + ok, token := checkToken(w, r) + if !ok { + return + } + resp, err := rp.Introspect(r.Context(), provider, token) + if err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + data, err := json.Marshal(resp) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write(data) + }) - // http.HandleFunc(protectedExchangeURL, func(w http.ResponseWriter, r *http.Request) { - // ok, token := checkToken(w, r) - // if !ok { - // return - // } - // tokens, err := provider.DelegationTokenExchange(r.Context(), token, oidc.WithResource([]string{"Test"})) - // if err != nil { - // http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) - // return - // } + //protected url which needs an active token and checks if the response of the introspect endpoint + //contains a requested claim with the required (string) value + //e.g. /protected/username/livio@caos.ch + router.HandleFunc(protectedClaimURL, func(w http.ResponseWriter, r *http.Request) { + ok, token := checkToken(w, r) + if !ok { + return + } + resp, err := rp.Introspect(r.Context(), provider, token) + if err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + params := mux.Vars(r) + requestedClaim := params["claim"] + requestedValue := params["value"] + value, ok := resp.GetClaim(requestedClaim).(string) + if !ok || value == "" || value != requestedValue { + http.Error(w, "claim does not match", http.StatusForbidden) + return + } + w.Write([]byte("authorized with value " + value)) + }) - // data, err := json.Marshal(tokens) - // if err != nil { - // http.Error(w, err.Error(), http.StatusInternalServerError) - // return - // } - // w.Write(data) - // }) - - // lis := fmt.Sprintf("127.0.0.1:%s", port) - // log.Printf("listening on http://%s/", lis) - // log.Fatal(http.ListenAndServe(lis, nil)) - // } - - // func checkToken(w http.ResponseWriter, r *http.Request) (bool, string) { - // token := r.Header.Get("authorization") - // if token == "" { - // http.Error(w, "Auth header missing", http.StatusUnauthorized) - // return false, "" - // } - // return true, token + lis := fmt.Sprintf("127.0.0.1:%s", port) + log.Printf("listening on http://%s/", lis) + log.Fatal(http.ListenAndServe(lis, router)) +} + +func checkToken(w http.ResponseWriter, r *http.Request) (bool, string) { + auth := r.Header.Get("authorization") + if auth == "" { + http.Error(w, "auth header missing", http.StatusUnauthorized) + return false, "" + } + if !strings.HasPrefix(auth, oidc.PrefixBearer) { + http.Error(w, "invalid header", http.StatusUnauthorized) + return false, "" + } + return true, strings.TrimPrefix(auth, oidc.PrefixBearer) } diff --git a/pkg/oidc/code_challenge.go b/pkg/oidc/code_challenge.go index 44a0499..9c4c8a3 100644 --- a/pkg/oidc/code_challenge.go +++ b/pkg/oidc/code_challenge.go @@ -24,7 +24,7 @@ func NewSHACodeChallenge(code string) string { func VerifyCodeChallenge(c *CodeChallenge, codeVerifier string) bool { if c == nil { - return false //TODO: ? + return false } if c.Method == CodeChallengeMethodS256 { codeVerifier = NewSHACodeChallenge(codeVerifier) diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go index 4621a1f..970369c 100644 --- a/pkg/oidc/discovery.go +++ b/pkg/oidc/discovery.go @@ -5,23 +5,23 @@ const ( ) type DiscoveryConfiguration struct { - Issuer string `json:"issuer,omitempty"` - AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` - TokenEndpoint string `json:"token_endpoint,omitempty"` - IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` - UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` - EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` - CheckSessionIframe string `json:"check_session_iframe,omitempty"` - JwksURI string `json:"jwks_uri,omitempty"` - ScopesSupported []string `json:"scopes_supported,omitempty"` - ResponseTypesSupported []string `json:"response_types_supported,omitempty"` - ResponseModesSupported []string `json:"response_modes_supported,omitempty"` - GrantTypesSupported []string `json:"grant_types_supported,omitempty"` - SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` - IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` - TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"` - CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported,omitempty"` - ClaimsSupported []string `json:"claims_supported,omitempty"` + Issuer string `json:"issuer,omitempty"` + AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` + TokenEndpoint string `json:"token_endpoint,omitempty"` + IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` + UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` + EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` + CheckSessionIframe string `json:"check_session_iframe,omitempty"` + JwksURI string `json:"jwks_uri,omitempty"` + ScopesSupported []string `json:"scopes_supported,omitempty"` + ResponseTypesSupported []string `json:"response_types_supported,omitempty"` + ResponseModesSupported []string `json:"response_modes_supported,omitempty"` + GrantTypesSupported []string `json:"grant_types_supported,omitempty"` + SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` + IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` + TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"` + CodeChallengeMethodsSupported []CodeChallengeMethod `json:"code_challenge_methods_supported,omitempty"` + ClaimsSupported []string `json:"claims_supported,omitempty"` } type AuthMethod string diff --git a/pkg/oidc/grants/tokenexchange/tokenexchange.go b/pkg/oidc/grants/tokenexchange/tokenexchange.go index 5cb6e79..02a9808 100644 --- a/pkg/oidc/grants/tokenexchange/tokenexchange.go +++ b/pkg/oidc/grants/tokenexchange/tokenexchange.go @@ -1,9 +1,5 @@ package tokenexchange -import ( - "github.com/caos/oidc/pkg/oidc" -) - const ( AccessTokenType = "urn:ietf:params:oauth:token-type:access_token" RefreshTokenType = "urn:ietf:params:oauth:token-type:refresh_token" @@ -26,22 +22,6 @@ type TokenExchangeRequest struct { requestedTokenType string `schema:"requested_token_type"` } -type JWTProfileRequest struct { - Assertion string `schema:"assertion"` - Scope oidc.Scopes `schema:"scope"` - GrantType oidc.GrantType `schema:"grant_type"` -} - -//ClientCredentialsGrantBasic creates an oauth2 `Client Credentials` Grant -//sneding client_id and client_secret as basic auth header -func NewJWTProfileRequest(assertion string, scopes ...string) *JWTProfileRequest { - return &JWTProfileRequest{ - GrantType: oidc.GrantTypeBearer, - Assertion: assertion, - Scope: scopes, - } -} - func NewTokenExchangeRequest(subjectToken, subjectTokenType string, opts ...TokenExchangeOption) *TokenExchangeRequest { t := &TokenExchangeRequest{ grantType: TokenExchangeGrantType, diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index 6414bef..98f3969 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -251,5 +251,9 @@ func (i *introspectionResponse) UnmarshalJSON(data []byte) error { i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC()) + if err := json.Unmarshal(data, &i.claims); err != nil { + return err + } + return nil } diff --git a/pkg/oidc/jwt_profile.go b/pkg/oidc/jwt_profile.go new file mode 100644 index 0000000..6969783 --- /dev/null +++ b/pkg/oidc/jwt_profile.go @@ -0,0 +1,18 @@ +package oidc + +type JWTProfileGrantRequest struct { + Assertion string `schema:"assertion"` + Scope Scopes `schema:"scope"` + GrantType GrantType `schema:"grant_type"` +} + +//NewJWTProfileGrantRequest creates an oauth2 `JSON Web Token (JWT) Profile` Grant +//`urn:ietf:params:oauth:grant-type:jwt-bearer` +//sending a self-signed jwt as assertion +func NewJWTProfileGrantRequest(assertion string, scopes ...string) *JWTProfileGrantRequest { + return &JWTProfileGrantRequest{ + GrantType: GrantTypeBearer, + Assertion: assertion, + Scope: scopes, + } +} diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index ff8f33e..1fc40b3 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -1,7 +1,10 @@ package oidc import ( + "crypto/rsa" + "crypto/x509" "encoding/json" + "encoding/pem" "io/ioutil" "time" @@ -14,6 +17,8 @@ import ( const ( //BearerToken defines the token_type `Bearer`, which is returned in a successful token response BearerToken = "Bearer" + + PrefixBearer = BearerToken + " " ) type Tokens struct { @@ -397,7 +402,7 @@ type AccessTokenResponse struct { type JWTProfileAssertion struct { PrivateKeyID string `json:"-"` PrivateKey []byte `json:"-"` - Issuer string `json:"issuer"` + Issuer string `json:"iss"` Subject string `json:"sub"` Audience Audience `json:"aud"` Expiration Time `json:"exp"` @@ -412,6 +417,19 @@ func NewJWTProfileAssertionFromKeyJSON(filename string, audience []string) (*JWT return NewJWTProfileAssertionFromFileData(data, audience) } +func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string) (string, error) { + keyData := new(struct { + KeyID string `json:"keyId"` + Key string `json:"key"` + UserID string `json:"userId"` + }) + err := json.Unmarshal(data, keyData) + if err != nil { + return "", err + } + return generateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key))) +} + func NewJWTProfileAssertionFromFileData(data []byte, audience []string) (*JWTProfileAssertion, error) { keyData := new(struct { KeyID string `json:"keyId"` @@ -454,3 +472,46 @@ func AppendClientIDToAudience(clientID string, audience []string) []string { } return append(audience, clientID) } + +func generateJWTProfileToken(assertion *JWTProfileAssertion) (string, error) { + privateKey, err := bytesToPrivateKey(assertion.PrivateKey) + if err != nil { + return "", err + } + key := jose.SigningKey{ + Algorithm: jose.RS256, + Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID}, + } + signer, err := jose.NewSigner(key, &jose.SignerOptions{}) + if err != nil { + return "", err + } + + marshalledAssertion, err := json.Marshal(assertion) + if err != nil { + return "", err + } + signedAssertion, err := signer.Sign(marshalledAssertion) + if err != nil { + return "", err + } + return signedAssertion.CompactSerialize() +} + +func bytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(priv) + enc := x509.IsEncryptedPEMBlock(block) + b := block.Bytes + var err error + if enc { + b, err = x509.DecryptPEMBlock(block, nil) + if err != nil { + return nil, err + } + } + key, err := x509.ParsePKCS1PrivateKey(b) + if err != nil { + return nil, err + } + return key, nil +} diff --git a/pkg/op/authrequest.go b/pkg/op/authrequest.go index 9e320f8..3a79b9b 100644 --- a/pkg/op/authrequest.go +++ b/pkg/op/authrequest.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "strings" + "time" "github.com/gorilla/mux" @@ -12,6 +13,23 @@ import ( "github.com/caos/oidc/pkg/utils" ) +type AuthRequest interface { + GetID() string + GetACR() string + GetAMR() []string + GetAudience() []string + GetAuthTime() time.Time + GetClientID() string + GetCodeChallenge() *oidc.CodeChallenge + GetNonce() string + GetRedirectURI() string + GetResponseType() oidc.ResponseType + GetScopes() []string + GetState() string + GetSubject() string + Done() bool +} + type Authorizer interface { Storage() Storage Decoder() utils.Decoder diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 8708fbb..2ac4833 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -122,10 +122,10 @@ func AuthMethods(c Configuration) []oidc.AuthMethod { return authMethods } -func CodeChallengeMethods(c Configuration) []string { - codeMethods := make([]string, 0, 1) +func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod { + codeMethods := make([]oidc.CodeChallengeMethod, 0, 1) if c.CodeMethodS256Supported() { - codeMethods = append(codeMethods, CodeMethodS256) + codeMethods = append(codeMethods, oidc.CodeChallengeMethodS256) } return codeMethods } diff --git a/pkg/op/discovery_test.go b/pkg/op/discovery_test.go index c14fac4..e479faa 100644 --- a/pkg/op/discovery_test.go +++ b/pkg/op/discovery_test.go @@ -215,7 +215,7 @@ func Test_AuthMethods(t *testing.T) { m.EXPECT().AuthMethodPostSupported().Return(false) return m }()}, - []string{string(op.AuthMethodBasic)}, + []string{string(oidc.AuthMethodBasic)}, }, { "basic and post", @@ -223,7 +223,7 @@ func Test_AuthMethods(t *testing.T) { m.EXPECT().AuthMethodPostSupported().Return(true) return m }()}, - []string{string(op.AuthMethodBasic), string(op.AuthMethodPost)}, + []string{string(oidc.AuthMethodBasic), string(oidc.AuthMethodPost)}, }, } for _, tt := range tests { diff --git a/pkg/op/op.go b/pkg/op/op.go index fa32a23..ee41630 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -17,27 +17,27 @@ import ( ) const ( - healthzEndpoint = "/healthz" + healthEndpoint = "/healthz" readinessEndpoint = "/ready" defaultAuthorizationEndpoint = "authorize" - defaulTokenEndpoint = "oauth/token" + defaultTokenEndpoint = "oauth/token" defaultIntrospectEndpoint = "oauth/introspect" defaultUserinfoEndpoint = "userinfo" defaultEndSessionEndpoint = "end_session" defaultKeysEndpoint = "keys" - AuthMethodBasic AuthMethod = "client_secret_basic" - AuthMethodPost AuthMethod = "client_secret_post" - AuthMethodNone AuthMethod = "none" - AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt" + //AuthMethodBasic AuthMethod = "client_secret_basic" + //AuthMethodPost AuthMethod = "client_secret_post" + //AuthMethodNone AuthMethod = "none" + //AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt" - CodeMethodS256 = "S256" + //CodeMethodS256 = "S256" ) var ( DefaultEndpoints = &endpoints{ Authorization: NewEndpoint(defaultAuthorizationEndpoint), - Token: NewEndpoint(defaulTokenEndpoint), + Token: NewEndpoint(defaultTokenEndpoint), Introspection: NewEndpoint(defaultIntrospectEndpoint), Userinfo: NewEndpoint(defaultUserinfoEndpoint), EndSession: NewEndpoint(defaultEndSessionEndpoint), @@ -73,7 +73,7 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router handlers.AllowedHeaders([]string{"authorization", "content-type"}), handlers.AllowedOriginValidator(allowAllOrigins), )) - router.HandleFunc(healthzEndpoint, healthzHandler) + router.HandleFunc(healthEndpoint, healthHandler) router.HandleFunc(readinessEndpoint, readyHandler(o.Probes())) router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Signer())) router.Handle(o.AuthorizationEndpoint().Relative(), intercept(authorizeHandler(o))) diff --git a/pkg/op/probes.go b/pkg/op/probes.go index 7dc00a9..c6bb748 100644 --- a/pkg/op/probes.go +++ b/pkg/op/probes.go @@ -10,7 +10,7 @@ import ( type ProbesFn func(context.Context) error -func healthzHandler(w http.ResponseWriter, r *http.Request) { +func healthHandler(w http.ResponseWriter, r *http.Request) { ok(w) } diff --git a/pkg/op/storage.go b/pkg/op/storage.go index b5f1dfe..c4da464 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -50,23 +50,6 @@ type StorageNotFoundError interface { IsNotFound() } -type AuthRequest interface { - GetID() string - GetACR() string - GetAMR() []string - GetAudience() []string - GetAuthTime() time.Time - GetClientID() string - GetCodeChallenge() *oidc.CodeChallenge - GetNonce() string - GetRedirectURI() string - GetResponseType() oidc.ResponseType - GetScopes() []string - GetState() string - GetSubject() string - Done() bool -} - type EndSessionRequest struct { UserID string Client Client diff --git a/pkg/op/tokenrequest.go b/pkg/op/tokenrequest.go index 0e295a3..1345005 100644 --- a/pkg/op/tokenrequest.go +++ b/pkg/op/tokenrequest.go @@ -7,7 +7,6 @@ import ( "net/url" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" "github.com/caos/oidc/pkg/utils" ) @@ -203,12 +202,12 @@ func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizati utils.MarshalJSON(w, resp) } -func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (*tokenexchange.JWTProfileRequest, error) { +func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (*oidc.JWTProfileGrantRequest, error) { err := r.ParseForm() if err != nil { return nil, ErrInvalidRequest("error parsing form") } - tokenReq := new(tokenexchange.JWTProfileRequest) + tokenReq := new(oidc.JWTProfileGrantRequest) err = decoder.Decode(tokenReq, r.Form) if err != nil { return nil, ErrInvalidRequest("error decoding form") diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index b30bdc5..03d8264 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -70,7 +70,7 @@ func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerif //TODO: implement delegation (openid core / oauth rfc) } - keySet := &jwtProfileKeySet{v.Storage(), request.Subject} + keySet := &jwtProfileKeySet{v.Storage(), request.Issuer} if err = oidc.CheckSignature(ctx, assertion, payload, request, nil, keySet); err != nil { return nil, err diff --git a/pkg/rp/key.go b/pkg/rp/key.go new file mode 100644 index 0000000..58a92d1 --- /dev/null +++ b/pkg/rp/key.go @@ -0,0 +1,33 @@ +package rp + +import ( + "encoding/json" + "io/ioutil" +) + +const ( + serviceAccountKey = "serviceaccount" + applicationKey = "application" +) + +type keyFile struct { + Type string `json:"type"` // serviceaccount or application + KeyID string `json:"keyId"` + Key string `json:"key"` + Issuer string `json:"issuer"` + ClientID string `json:"clientId"` + //TokenURL string `json:"token_uri"` + //ProjectID string `json:"project_id"` +} + +func ConfigFromKeyFile(path string) (*keyFile, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + var f keyFile + if err := json.Unmarshal(data, &f); err != nil { + return nil, err + } + return &f, nil +} diff --git a/pkg/rp/relaying_party.go b/pkg/rp/relaying_party.go index 6807221..be62762 100644 --- a/pkg/rp/relaying_party.go +++ b/pkg/rp/relaying_party.go @@ -53,6 +53,9 @@ type RelayingParty interface { //IsOAuth2Only specifies whether relaying party handles only oauth2 or oidc calls IsOAuth2Only() bool + ClientKey() []byte + ClientKeyID() string + //IDTokenVerifier returns the verifier interface used for oidc id_token verification IDTokenVerifier() IDTokenVerifier @@ -74,11 +77,13 @@ type relayingParty struct { oauthConfig *oauth2.Config oauth2Only bool pkce bool + clientKey []byte + clientKeyID string httpClient *http.Client cookieHandler *utils.CookieHandler - errorHandler func(http.ResponseWriter, *http.Request, string, string, string) + errorHandler func(http.ResponseWriter, *http.Request, string, string, string) idTokenVerifier IDTokenVerifier verifierOpts []VerifierOption } @@ -103,6 +108,14 @@ func (rp *relayingParty) IsOAuth2Only() bool { return rp.oauth2Only } +func (rp *relayingParty) ClientKey() []byte { + return rp.clientKey +} + +func (rp *relayingParty) ClientKeyID() string { + return rp.clientKeyID +} + func (rp *relayingParty) IDTokenVerifier() IDTokenVerifier { if rp.idTokenVerifier == nil { rp.idTokenVerifier = NewIDTokenVerifier(rp.issuer, rp.oauthConfig.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...) @@ -314,6 +327,14 @@ func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc } codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier)) } + //if len(rp.ClientKey()) > 0 { + // assertion, err := oidc.NewJWTProfileAssertionStringFromFileData(rp.ClientKey(), []string{rp.OAuthConfig().Endpoint.TokenURL}) + // if err != nil { + // http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized) + // return + // } + // codeOpts = append(codeOpts, WithClientAssertionJWT(assertion)) + //} tokens, err := CodeExchange(r.Context(), params.Get("code"), rp, codeOpts...) if err != nil { http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) @@ -439,3 +460,13 @@ func WithCodeVerifier(codeVerifier string) CodeExchangeOpt { return []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("code_verifier", codeVerifier)} } } + +//WithClientAssertionJWT sets the `client_assertion` param in the token request +func WithClientAssertionJWT(clientAssertion string) CodeExchangeOpt { + return func() []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam("client_assertion", clientAssertion), + oauth2.SetAuthURLParam("client_assertion_type", oidc.ClientAssertionTypeJWTAssertion), + } + } +} diff --git a/pkg/rp/resource_server.go b/pkg/rp/resource_server.go new file mode 100644 index 0000000..e0488f1 --- /dev/null +++ b/pkg/rp/resource_server.go @@ -0,0 +1,118 @@ +package rp + +import ( + "context" + "errors" + "net/http" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" + "golang.org/x/oauth2/jwt" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +type ResourceServer interface { + IntrospectionURL() string + HttpClient() *http.Client +} + +type resourceServer struct { + issuer string + tokenURL string + introspectURL string + httpClient *http.Client +} + +func (r *resourceServer) IntrospectionURL() string { + return r.introspectURL +} + +func (r *resourceServer) HttpClient() *http.Client { + return r.httpClient +} + +func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option RSOption) (ResourceServer, error) { + authorizer := func(tokenURL string) func(ctx context.Context) *http.Client { + return (&clientcredentials.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + TokenURL: tokenURL, + }).Client + } + return newResourceServer(issuer, authorizer, option) +} +func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...RSOption) (ResourceServer, error) { + authorizer := func(tokenURL string) func(ctx context.Context) *http.Client { + return (&jwt.Config{ + Email: clientID, + Subject: clientID, + PrivateKey: key, + PrivateKeyID: keyID, + Audience: issuer, + TokenURL: tokenURL, + }).Client + } + return newResourceServer(issuer, authorizer, options...) +} + +func newResourceServer(issuer string, authorizer func(tokenURL string) func(ctx context.Context) *http.Client, options ...RSOption) (*resourceServer, error) { + rp := &resourceServer{ + issuer: issuer, + httpClient: utils.DefaultHTTPClient, + } + for _, optFunc := range options { + optFunc(rp) + } + if rp.introspectURL == "" || rp.tokenURL == "" { + endpoints, err := Discover(rp.issuer, rp.httpClient) + if err != nil { + return nil, err + } + rp.tokenURL = endpoints.TokenURL + rp.introspectURL = endpoints.IntrospectURL + } + if rp.introspectURL == "" || rp.tokenURL == "" { + return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url") + } + rp.httpClient = authorizer(rp.tokenURL)(context.WithValue(context.Background(), oauth2.HTTPClient, rp.HttpClient())) + return rp, nil +} + +func NewResourceServerFromKeyFile(path string, options ...RSOption) (ResourceServer, error) { + c, err := ConfigFromKeyFile(path) + if err != nil { + return nil, err + } + return NewResourceServerJWTProfile(c.Issuer, c.ClientID, c.KeyID, []byte(c.Key), options...) +} + +type RSOption func(*resourceServer) + +//WithClient provides the ability to set an http client to be used for the resource server +func WithClient(client *http.Client) RSOption { + return func(server *resourceServer) { + server.httpClient = client + } +} + +//WithStaticEndpoints provides the ability to set static token and introspect URL +func WithStaticEndpoints(tokenURL, introspectURL string) RSOption { + return func(server *resourceServer) { + server.tokenURL = tokenURL + server.introspectURL = introspectURL + } +} + +func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.IntrospectionResponse, error) { + req, err := utils.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, encoder, nil) + if err != nil { + return nil, err + } + resp := oidc.NewIntrospectionResponse() + if err := utils.HttpRequest(rp.HttpClient(), req, resp); err != nil { + return nil, err + } + return resp, nil +} diff --git a/pkg/rp/tockenexchange.go b/pkg/rp/tockenexchange.go index 4396dc4..98bd841 100644 --- a/pkg/rp/tockenexchange.go +++ b/pkg/rp/tockenexchange.go @@ -43,8 +43,8 @@ func DelegationTokenExchange(ctx context.Context, subjectToken string, rp Relayi } //JWTProfileExchange handles the oauth2 jwt profile exchange -func JWTProfileExchange(ctx context.Context, jwtProfileRequest *tokenexchange.JWTProfileRequest, rp RelayingParty) (*oauth2.Token, error) { - return CallTokenEndpoint(jwtProfileRequest, rp) +func JWTProfileExchange(ctx context.Context, jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, rp RelayingParty) (*oauth2.Token, error) { + return CallTokenEndpoint(jwtProfileGrantRequest, rp) } //JWTProfileExchange handles the oauth2 jwt profile exchange @@ -53,7 +53,7 @@ func JWTProfileAssertionExchange(ctx context.Context, assertion *oidc.JWTProfile if err != nil { return nil, err } - return JWTProfileExchange(ctx, tokenexchange.NewJWTProfileRequest(token, scopes...), rp) + return JWTProfileExchange(ctx, oidc.NewJWTProfileGrantRequest(token, scopes...), rp) } func generateJWTProfileToken(assertion *oidc.JWTProfileAssertion) (string, error) { From 4b426c899a4e29374f37b35430787d89dbb782c8 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Tue, 2 Feb 2021 11:41:50 +0100 Subject: [PATCH 019/502] scopes --- pkg/op/tokenrequest.go | 20 +++++++++++++++++++- pkg/rp/key.go | 15 +++++++++------ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/pkg/op/tokenrequest.go b/pkg/op/tokenrequest.go index 1345005..5cb872e 100644 --- a/pkg/op/tokenrequest.go +++ b/pkg/op/tokenrequest.go @@ -192,7 +192,7 @@ func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizati } //TODO: filter scopes - tokenRequest.Scopes = profileRequest.Scope + tokenRequest.Scopes = ValidateJWTProfileScopes(tokenRequest., profileRequest.Scope) resp, err := CreateJWTTokenResponse(r.Context(), tokenRequest, exchanger) if err != nil { @@ -215,6 +215,24 @@ func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (*oidc.JWTPr return tokenReq, nil } +func ValidateJWTProfileScopes(client Client, scopes []string) []string { + for i := len(scopes) - 1; i >= 0; i-- { + scope := scopes[i] + if !(scope == oidc.ScopeOpenID || + scope == oidc.ScopeProfile || + scope == oidc.ScopeEmail || + scope == oidc.ScopePhone || + scope == oidc.ScopeAddress || + scope == oidc.ScopeOfflineAccess) && //TODO: allowed + !client.IsScopeAllowed(scope) { + scopes[i] = scopes[len(scopes)-1] + scopes[len(scopes)-1] = "" + scopes = scopes[:len(scopes)-1] + } + } + return scopes +} + func TokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { tokenRequest, err := ParseTokenExchangeRequest(w, r) if err != nil { diff --git a/pkg/rp/key.go b/pkg/rp/key.go index 58a92d1..26d8bf5 100644 --- a/pkg/rp/key.go +++ b/pkg/rp/key.go @@ -11,13 +11,16 @@ const ( ) type keyFile struct { - Type string `json:"type"` // serviceaccount or application - KeyID string `json:"keyId"` - Key string `json:"key"` - Issuer string `json:"issuer"` + Type string `json:"type"` // serviceaccount or application + KeyID string `json:"keyId"` + Key string `json:"key"` + Issuer string `json:"issuer"` + + //serviceaccount + UserID string `json:"userId"` + + //application ClientID string `json:"clientId"` - //TokenURL string `json:"token_uri"` - //ProjectID string `json:"project_id"` } func ConfigFromKeyFile(path string) (*keyFile, error) { From 345fc7e837156fd1bdce8bcad423f8eb448b9395 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Wed, 3 Feb 2021 10:42:01 +0100 Subject: [PATCH 020/502] token introspection --- pkg/oidc/introspection.go | 11 +++++++---- pkg/op/storage.go | 3 ++- pkg/op/token_intospection.go | 23 ++++++++++++++--------- pkg/op/tokenrequest.go | 26 +++++--------------------- 4 files changed, 28 insertions(+), 35 deletions(-) diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index 98f3969..98a8e25 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -6,8 +6,6 @@ import ( "time" "golang.org/x/text/language" - - "github.com/caos/oidc/pkg/utils" ) type IntrospectionRequest struct { @@ -230,11 +228,16 @@ func (i *introspectionResponse) MarshalJSON() ([]byte, error) { return b, nil } - claims, err := json.Marshal(i.claims) + err = json.Unmarshal(b, &i.claims) if err != nil { return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims) } - return utils.ConcatenateJSON(b, claims) + + return json.Marshal(i.claims) + //if err != nil { + // return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims) + //} + //return utils.ConcatenateJSON(b, claims) } func (i *introspectionResponse) UnmarshalJSON(data []byte) error { diff --git a/pkg/op/storage.go b/pkg/op/storage.go index c4da464..4072630 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -30,9 +30,10 @@ type OPStorage interface { AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error + SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, callerTokenID, callerSubject string) error GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) - //ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) + ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) //deprecated: use GetUserinfoFromScopes instead GetUserinfoFromScopes(ctx context.Context, userID, clientID string, scopes []string) (oidc.UserInfo, error) diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go index e6a5d4a..8ea4dbb 100644 --- a/pkg/op/token_intospection.go +++ b/pkg/op/token_intospection.go @@ -3,6 +3,7 @@ package op import ( "errors" "net/http" + "strings" "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/utils" @@ -22,20 +23,24 @@ func introspectionHandler(introspector Introspector) func(http.ResponseWriter, * } func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspector) { - //validate authorization - + callerToken := r.Header.Get("authorization") response := oidc.NewIntrospectionResponse() - token, err := ParseTokenInrospectionRequest(r, introspector.Decoder()) - if err != nil { - utils.MarshalJSON(w, response) - return - } - tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, token) + callerToken, callerSubject, ok := getTokenIDAndSubject(r.Context(), introspector, strings.TrimPrefix(callerToken, oidc.PrefixBearer)) if !ok { utils.MarshalJSON(w, response) return } - err = introspector.Storage().SetUserinfoFromToken(r.Context(), response, tokenID, subject, r.Header.Get("origin")) + introspectionToken, err := ParseTokenInrospectionRequest(r, introspector.Decoder()) + if err != nil { + utils.MarshalJSON(w, response) + return + } + tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, introspectionToken) + if !ok { + utils.MarshalJSON(w, response) + return + } + err = introspector.Storage().SetIntrospectionFromToken(r.Context(), response, tokenID, subject, callerToken, callerSubject) if err != nil { utils.MarshalJSON(w, response) return diff --git a/pkg/op/tokenrequest.go b/pkg/op/tokenrequest.go index 5cb872e..e0729bf 100644 --- a/pkg/op/tokenrequest.go +++ b/pkg/op/tokenrequest.go @@ -191,9 +191,11 @@ func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizati return } - //TODO: filter scopes - tokenRequest.Scopes = ValidateJWTProfileScopes(tokenRequest., profileRequest.Scope) - + tokenRequest.Scopes, err = exchanger.Storage().ValidateJWTProfileScopes(r.Context(), tokenRequest.Issuer, profileRequest.Scope) + if err != nil { + RequestError(w, r, err) + return + } resp, err := CreateJWTTokenResponse(r.Context(), tokenRequest, exchanger) if err != nil { RequestError(w, r, err) @@ -215,24 +217,6 @@ func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (*oidc.JWTPr return tokenReq, nil } -func ValidateJWTProfileScopes(client Client, scopes []string) []string { - for i := len(scopes) - 1; i >= 0; i-- { - scope := scopes[i] - if !(scope == oidc.ScopeOpenID || - scope == oidc.ScopeProfile || - scope == oidc.ScopeEmail || - scope == oidc.ScopePhone || - scope == oidc.ScopeAddress || - scope == oidc.ScopeOfflineAccess) && //TODO: allowed - !client.IsScopeAllowed(scope) { - scopes[i] = scopes[len(scopes)-1] - scopes[len(scopes)-1] = "" - scopes = scopes[:len(scopes)-1] - } - } - return scopes -} - func TokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { tokenRequest, err := ParseTokenExchangeRequest(w, r) if err != nil { From fa92a206156ee1946836249471e447662b161c7b Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Wed, 3 Feb 2021 13:04:06 +0100 Subject: [PATCH 021/502] fix: make GenerateJWTProfileToken public (#82) --- pkg/rp/tockenexchange.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/rp/tockenexchange.go b/pkg/rp/tockenexchange.go index 4396dc4..0fa3725 100644 --- a/pkg/rp/tockenexchange.go +++ b/pkg/rp/tockenexchange.go @@ -49,14 +49,14 @@ func JWTProfileExchange(ctx context.Context, jwtProfileRequest *tokenexchange.JW //JWTProfileExchange handles the oauth2 jwt profile exchange func JWTProfileAssertionExchange(ctx context.Context, assertion *oidc.JWTProfileAssertion, scopes oidc.Scopes, rp RelayingParty) (*oauth2.Token, error) { - token, err := generateJWTProfileToken(assertion) + token, err := GenerateJWTProfileToken(assertion) if err != nil { return nil, err } return JWTProfileExchange(ctx, tokenexchange.NewJWTProfileRequest(token, scopes...), rp) } -func generateJWTProfileToken(assertion *oidc.JWTProfileAssertion) (string, error) { +func GenerateJWTProfileToken(assertion *oidc.JWTProfileAssertion) (string, error) { privateKey, err := bytesToPrivateKey(assertion.PrivateKey) if err != nil { return "", err From 138da8a208ed248452b77ed114ddac348f1996c5 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Wed, 10 Feb 2021 16:42:01 +0100 Subject: [PATCH 022/502] introspect --- example/client/app/app.go | 18 +++++-- pkg/oidc/discovery.go | 67 ++++++++++++++++++------- pkg/oidc/introspection.go | 26 ++++++++-- pkg/oidc/token.go | 4 +- pkg/oidc/types.go | 14 ++++++ pkg/oidc/userinfo.go | 16 ++++-- pkg/op/discovery.go | 57 ++++++++++++++++------ pkg/op/storage.go | 2 +- pkg/op/token_intospection.go | 50 ++++++++++++------- pkg/op/userinfo.go | 28 +++++++---- pkg/rp/relaying_party.go | 24 ++++++--- pkg/rp/resource_server.go | 94 ++++++++++++++++++++++++++++++------ pkg/utils/http.go | 3 ++ 13 files changed, 305 insertions(+), 98 deletions(-) diff --git a/example/client/app/app.go b/example/client/app/app.go index ad00adb..c3bf9e0 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -27,6 +27,7 @@ var ( func main() { clientID := os.Getenv("CLIENT_ID") clientSecret := os.Getenv("CLIENT_SECRET") + keyPath := os.Getenv("KEY_PATH") issuer := os.Getenv("ISSUER") port := os.Getenv("PORT") scopes := strings.Split(os.Getenv("SCOPES"), " ") @@ -35,10 +36,19 @@ func main() { redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath) cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) - provider, err := rp.NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, - rp.WithPKCE(cookieHandler), - rp.WithVerifierOpts(rp.WithIssuedAtOffset(5*time.Second)), - ) + + options := []rp.Option{ + rp.WithCookieHandler(cookieHandler), + rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)), + } + if clientSecret == "" { + options = append(options, rp.WithPKCE(cookieHandler)) + } + if keyPath != "" { + options = append(options, rp.WithClientKey(keyPath)) + } + + provider, err := rp.NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...) if err != nil { logrus.Fatalf("error creating provider %s", err.Error()) } diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go index 970369c..104078c 100644 --- a/pkg/oidc/discovery.go +++ b/pkg/oidc/discovery.go @@ -1,27 +1,56 @@ package oidc +import ( + "golang.org/x/text/language" +) + const ( DiscoveryEndpoint = "/.well-known/openid-configuration" ) type DiscoveryConfiguration struct { - Issuer string `json:"issuer,omitempty"` - AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` - TokenEndpoint string `json:"token_endpoint,omitempty"` - IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` - UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` - EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` - CheckSessionIframe string `json:"check_session_iframe,omitempty"` - JwksURI string `json:"jwks_uri,omitempty"` - ScopesSupported []string `json:"scopes_supported,omitempty"` - ResponseTypesSupported []string `json:"response_types_supported,omitempty"` - ResponseModesSupported []string `json:"response_modes_supported,omitempty"` - GrantTypesSupported []string `json:"grant_types_supported,omitempty"` - SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` - IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` - TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"` - CodeChallengeMethodsSupported []CodeChallengeMethod `json:"code_challenge_methods_supported,omitempty"` - ClaimsSupported []string `json:"claims_supported,omitempty"` + Issuer string `json:"issuer,omitempty"` + AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` + TokenEndpoint string `json:"token_endpoint,omitempty"` + IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` + UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` + RevocationEndpoint string `json:"revocation_endpoint,omitempty"` + EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` + CheckSessionIframe string `json:"check_session_iframe,omitempty"` + JwksURI string `json:"jwks_uri,omitempty"` + ScopesSupported []string `json:"scopes_supported,omitempty"` + ResponseTypesSupported []string `json:"response_types_supported"` + ResponseModesSupported []string `json:"response_modes_supported,omitempty"` + GrantTypesSupported []GrantType `json:"grant_types_supported,omitempty"` + ACRValuesSupported []string `json:"acr_values_supported,omitempty"` + SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` + IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` + IDTokenEncryptionAlgValuesSupported []string `json:"id_token_encryption_alg_values_supported,omitempty"` + IDTokenEncryptionEncValuesSupported []string `json:"id_token_encryption_enc_values_supported,omitempty"` + UserinfoSigningAlgValuesSupported []string `json:"userinfo_signing_alg_values_supported,omitempty"` + UserinfoEncryptionAlgValuesSupported []string `json:"userinfo_encryption_alg_values_supported,omitempty"` + UserinfoEncryptionEncValuesSupported []string `json:"userinfo_encryption_enc_values_supported,omitempty"` + RequestObjectSigningAlgValuesSupported []string `json:"request_object_signing_alg_values_supported,omitempty"` + RequestObjectEncryptionAlgValuesSupported []string `json:"request_object_encryption_alg_values_supported,omitempty"` + RequestObjectEncryptionEncValuesSupported []string `json:"request_object_encryption_enc_values_supported,omitempty"` + TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"` + TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"` + RevocationEndpointAuthMethodsSupported []AuthMethod `json:"revocation_endpoint_auth_methods_supported,omitempty"` + RevocationEndpointAuthSigningAlgValuesSupported []string `json:"revocation_endpoint_auth_signing_alg_values_supported,omitempty"` + IntrospectionEndpointAuthMethodsSupported []AuthMethod `json:"introspection_endpoint_auth_methods_supported,omitempty"` + IntrospectionEndpointAuthSigningAlgValuesSupported []string `json:"introspection_endpoint_auth_signing_alg_values_supported,omitempty"` + DisplayValuesSupported []Display `json:"display_values_supported,omitempty"` + ClaimTypesSupported []string `json:"claim_types_supported,omitempty"` + ClaimsSupported []string `json:"claims_supported,omitempty"` + CodeChallengeMethodsSupported []CodeChallengeMethod `json:"code_challenge_methods_supported,omitempty"` + ServiceDocumentation string `json:"service_documentation,omitempty"` + ClaimsLocalesSupported []language.Tag `json:"claims_locales_supported,omitempty"` + UILocalesSupported []language.Tag `json:"ui_locales_supported,omitempty"` + RequestParameterSupported bool `json:"request_parameter_supported,omitempty"` + RequestURIParameterSupported bool `json:"request_uri_parameter_supported"` //no omitempty because: If omitted, the default value is true + RequireRequestURIRegistration bool `json:"require_request_uri_registration,omitempty"` + OPPolicyURI string `json:"op_policy_uri,omitempty"` + OPTermsOfServiceURI string `json:"op_tos_uri,omitempty"` } type AuthMethod string @@ -32,3 +61,7 @@ const ( AuthMethodNone AuthMethod = "none" AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt" ) + +const ( + GrantTypeImplicit GrantType = "implicit" +) diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index 98a8e25..1a66520 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -12,10 +12,17 @@ type IntrospectionRequest struct { Token string `schema:"token"` } +type ClientAssertionParams struct { + ClientAssertion string `schema:"client_assertion"` + ClientAssertionType string `schema:"client_assertion_type"` +} + type IntrospectionResponse interface { UserInfoSetter SetActive(bool) IsActive() bool + SetScopes(scopes Scope) + SetClientID(id string) } func NewIntrospectionResponse() IntrospectionResponse { @@ -23,19 +30,30 @@ func NewIntrospectionResponse() IntrospectionResponse { } type introspectionResponse struct { - Active bool `json:"active"` - Subject string `json:"sub,omitempty"` + Active bool `json:"active"` + Scope Scope `json:"scope,omitempty"` + ClientID string `json:"client_id,omitempty"` + Subject string `json:"sub,omitempty"` userInfoProfile userInfoEmail userInfoPhone - Address UserInfoAddress `json:"address,omitempty"` - claims map[string]interface{} + Address UserInfoAddress `json:"address,omitempty"` + claims map[string]interface{} } func (u *introspectionResponse) IsActive() bool { return u.Active } + +func (u *introspectionResponse) SetScopes(scope Scope) { + u.Scope = scope +} + +func (u *introspectionResponse) SetClientID(id string) { + u.ClientID = id +} + func (u *introspectionResponse) GetSubject() string { return u.Subject } diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index 1fc40b3..068e8e6 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -427,7 +427,7 @@ func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string) (s if err != nil { return "", err } - return generateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key))) + return GenerateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key))) } func NewJWTProfileAssertionFromFileData(data []byte, audience []string) (*JWTProfileAssertion, error) { @@ -473,7 +473,7 @@ func AppendClientIDToAudience(clientID string, audience []string) []string { return append(audience, clientID) } -func generateJWTProfileToken(assertion *JWTProfileAssertion) (string, error) { +func GenerateJWTProfileToken(assertion *JWTProfileAssertion) (string, error) { privateKey, err := bytesToPrivateKey(assertion.PrivateKey) if err != nil { return "", err diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index 86e5d06..fd496da 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -59,6 +59,7 @@ type Prompt string type ResponseType string type Scopes []string +type Scope []string //TODO: hurst? func (s Scopes) Encode() string { return strings.Join(s, " ") @@ -73,6 +74,19 @@ func (s *Scopes) MarshalText() ([]byte, error) { return []byte(s.Encode()), nil } +func (s *Scope) MarshalJSON() ([]byte, error) { + return json.Marshal(Scopes(*s).Encode()) +} + +func (s *Scope) UnmarshalJSON(data []byte) error { + var str string + if err := json.Unmarshal(data, &str); err != nil { + return err + } + *s = Scope(strings.Split(str, " ")) + return nil +} + type Time time.Time func (t *Time) UnmarshalJSON(data []byte) error { diff --git a/pkg/oidc/userinfo.go b/pkg/oidc/userinfo.go index 3c77b7b..3a92501 100644 --- a/pkg/oidc/userinfo.go +++ b/pkg/oidc/userinfo.go @@ -6,8 +6,6 @@ import ( "time" "golang.org/x/text/language" - - "github.com/caos/oidc/pkg/utils" ) type UserInfo interface { @@ -351,11 +349,17 @@ func (i *userinfo) MarshalJSON() ([]byte, error) { return b, nil } - claims, err := json.Marshal(i.claims) + err = json.Unmarshal(b, &i.claims) if err != nil { return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims) } - return utils.ConcatenateJSON(b, claims) + + return json.Marshal(i.claims) + //claims, err := json.Marshal(i.claims) + //if err != nil { + // return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims) + //} + //return utils.ConcatenateJSON(b, claims) } func (i *userinfo) UnmarshalJSON(data []byte) error { @@ -372,6 +376,10 @@ func (i *userinfo) UnmarshalJSON(data []byte) error { i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC()) + if err := json.Unmarshal(data, &i.claims); err != nil { + return err + } + return nil } diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 2ac4833..291214c 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -3,6 +3,8 @@ package op import ( "net/http" + "golang.org/x/text/language" + "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/utils" ) @@ -24,17 +26,22 @@ func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfigurati TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()), IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()), UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()), - EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), + //RevocationEndpoint: c.RevocationEndpoint().Absolute(c.Issuer()), + EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), // CheckSessionIframe: c.TokenEndpoint().Absolute(c.Issuer())(c.CheckSessionIframe), - JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), - ScopesSupported: Scopes(c), - ResponseTypesSupported: ResponseTypes(c), - GrantTypesSupported: GrantTypes(c), - ClaimsSupported: SupportedClaims(c), - IDTokenSigningAlgValuesSupported: SigAlgorithms(s), - SubjectTypesSupported: SubjectTypes(c), - TokenEndpointAuthMethodsSupported: AuthMethods(c), - CodeChallengeMethodsSupported: CodeChallengeMethods(c), + JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), + ScopesSupported: Scopes(c), + ResponseTypesSupported: ResponseTypes(c), + //ResponseModesSupported: + GrantTypesSupported: GrantTypes(c), + //ACRValuesSupported: ACRValues(c), + SubjectTypesSupported: SubjectTypes(c), + IDTokenSigningAlgValuesSupported: SigAlgorithms(s), + TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(c), + IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(c), + ClaimsSupported: SupportedClaims(c), + CodeChallengeMethodsSupported: CodeChallengeMethods(c), + UILocalesSupported: UILocales(c), } } @@ -58,15 +65,16 @@ func ResponseTypes(c Configuration) []string { } //TODO: ok for now, check later if dynamic needed } -func GrantTypes(c Configuration) []string { - grantTypes := []string{ - string(oidc.GrantTypeCode), +func GrantTypes(c Configuration) []oidc.GrantType { + grantTypes := []oidc.GrantType{ + oidc.GrantTypeCode, + oidc.GrantTypeImplicit, } if c.GrantTypeTokenExchangeSupported() { - grantTypes = append(grantTypes, string(oidc.GrantTypeTokenExchange)) + grantTypes = append(grantTypes, oidc.GrantTypeTokenExchange) } if c.GrantTypeJWTAuthorizationSupported() { - grantTypes = append(grantTypes, string(oidc.GrantTypeBearer)) + grantTypes = append(grantTypes, oidc.GrantTypeBearer) } return grantTypes } @@ -108,7 +116,7 @@ func SubjectTypes(c Configuration) []string { return []string{"public"} //TODO: config } -func AuthMethods(c Configuration) []oidc.AuthMethod { +func AuthMethodsTokenEndpoint(c Configuration) []oidc.AuthMethod { authMethods := []oidc.AuthMethod{ oidc.AuthMethodNone, oidc.AuthMethodBasic, @@ -122,6 +130,16 @@ func AuthMethods(c Configuration) []oidc.AuthMethod { return authMethods } +func AuthMethodsIntrospectionEndpoint(c Configuration) []oidc.AuthMethod { + authMethods := []oidc.AuthMethod{ + oidc.AuthMethodBasic, + } + if c.AuthMethodPrivateKeyJWTSupported() { + authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT) + } + return authMethods +} + func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod { codeMethods := make([]oidc.CodeChallengeMethod, 0, 1) if c.CodeMethodS256Supported() { @@ -129,3 +147,10 @@ func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod { } return codeMethods } + +func UILocales(c Configuration) []language.Tag { + return []language.Tag{ + language.English, + language.German, + } +} diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 4072630..967a24c 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -30,7 +30,7 @@ type OPStorage interface { AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error - SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, callerTokenID, callerSubject string) error + SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, clientID string) error GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go index 8ea4dbb..30d2544 100644 --- a/pkg/op/token_intospection.go +++ b/pkg/op/token_intospection.go @@ -3,7 +3,6 @@ package op import ( "errors" "net/http" - "strings" "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/utils" @@ -16,6 +15,11 @@ type Introspector interface { AccessTokenVerifier() AccessTokenVerifier } +type IntrospectorJWTProfile interface { + Introspector + JWTProfileVerifier() JWTProfileVerifier +} + func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { Introspect(w, r, introspector) @@ -23,24 +27,18 @@ func introspectionHandler(introspector Introspector) func(http.ResponseWriter, * } func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspector) { - callerToken := r.Header.Get("authorization") response := oidc.NewIntrospectionResponse() - callerToken, callerSubject, ok := getTokenIDAndSubject(r.Context(), introspector, strings.TrimPrefix(callerToken, oidc.PrefixBearer)) - if !ok { - utils.MarshalJSON(w, response) - return - } - introspectionToken, err := ParseTokenInrospectionRequest(r, introspector.Decoder()) + token, clientID, err := ParseTokenIntrospectionRequest(r, introspector) if err != nil { - utils.MarshalJSON(w, response) + http.Error(w, err.Error(), http.StatusUnauthorized) return } - tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, introspectionToken) + tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, token) if !ok { utils.MarshalJSON(w, response) return } - err = introspector.Storage().SetIntrospectionFromToken(r.Context(), response, tokenID, subject, callerToken, callerSubject) + err = introspector.Storage().SetIntrospectionFromToken(r.Context(), response, tokenID, subject, clientID) if err != nil { utils.MarshalJSON(w, response) return @@ -49,15 +47,31 @@ func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspecto utils.MarshalJSON(w, response) } -func ParseTokenInrospectionRequest(r *http.Request, decoder utils.Decoder) (string, error) { - err := r.ParseForm() +func ParseTokenIntrospectionRequest(r *http.Request, introspector Introspector) (token, clientID string, err error) { + err = r.ParseForm() if err != nil { - return "", errors.New("unable to parse request") + return "", "", errors.New("unable to parse request") } - req := new(oidc.IntrospectionRequest) - err = decoder.Decode(req, r.Form) + req := new(struct { + oidc.IntrospectionRequest + oidc.ClientAssertionParams + }) + err = introspector.Decoder().Decode(req, r.Form) if err != nil { - return "", errors.New("unable to parse request") + return "", "", errors.New("unable to parse request") } - return req.Token, nil + if introspectorJWTProfile, ok := introspector.(IntrospectorJWTProfile); ok && req.ClientAssertion != "" { + profile, err := VerifyJWTAssertion(r.Context(), req.ClientAssertion, introspectorJWTProfile.JWTProfileVerifier()) + if err == nil { + return req.Token, profile.Issuer, nil + } + } + clientID, clientSecret, ok := r.BasicAuth() + if ok { + if err := introspector.Storage().AuthorizeClientIDSecret(r.Context(), clientID, clientSecret); err != nil { + return "", "", err + } + return req.Token, clientID, nil + } + return "", "", errors.New("invalid authorization") } diff --git a/pkg/op/userinfo.go b/pkg/op/userinfo.go index 1163598..d951136 100644 --- a/pkg/op/userinfo.go +++ b/pkg/op/userinfo.go @@ -24,7 +24,7 @@ func userinfoHandler(userinfoProvider UserinfoProvider) func(http.ResponseWriter } func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoProvider) { - accessToken, err := getAccessToken(r, userinfoProvider.Decoder()) + accessToken, err := ParseUserinfoRequest(r, userinfoProvider.Decoder()) if err != nil { http.Error(w, "access token missing", http.StatusUnauthorized) return @@ -43,16 +43,12 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP utils.MarshalJSON(w, info) } -func getAccessToken(r *http.Request, decoder utils.Decoder) (string, error) { - authHeader := r.Header.Get("authorization") - if authHeader != "" { - parts := strings.Split(authHeader, "Bearer ") - if len(parts) != 2 { - return "", errors.New("invalid auth header") - } - return parts[1], nil +func ParseUserinfoRequest(r *http.Request, decoder utils.Decoder) (string, error) { + accessToken, err := getAccessToken(r) + if err == nil { + return accessToken, nil } - err := r.ParseForm() + err = r.ParseForm() if err != nil { return "", errors.New("unable to parse request") } @@ -64,6 +60,18 @@ func getAccessToken(r *http.Request, decoder utils.Decoder) (string, error) { return req.AccessToken, nil } +func getAccessToken(r *http.Request) (string, error) { + authHeader := r.Header.Get("authorization") + if authHeader == "" { + return "", errors.New("no auth header") + } + parts := strings.Split(authHeader, "Bearer ") + if len(parts) != 2 { + return "", errors.New("invalid auth header") + } + return parts[1], nil +} + func getTokenIDAndSubject(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, bool) { tokenIDSubject, err := userinfoProvider.Crypto().Decrypt(accessToken) if err == nil { diff --git a/pkg/rp/relaying_party.go b/pkg/rp/relaying_party.go index be62762..3260657 100644 --- a/pkg/rp/relaying_party.go +++ b/pkg/rp/relaying_party.go @@ -216,6 +216,14 @@ func WithVerifierOpts(opts ...VerifierOption) Option { } } +func WithClientKey(path string) Option { + return func(rp *relayingParty) { + config, _ := ConfigFromKeyFile(path) + rp.clientKey = []byte(config.Key) + rp.clientKeyID = config.KeyID + } +} + //Discover calls the discovery endpoint of the provided issuer and returns the found endpoints func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint @@ -327,14 +335,14 @@ func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc } codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier)) } - //if len(rp.ClientKey()) > 0 { - // assertion, err := oidc.NewJWTProfileAssertionStringFromFileData(rp.ClientKey(), []string{rp.OAuthConfig().Endpoint.TokenURL}) - // if err != nil { - // http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized) - // return - // } - // codeOpts = append(codeOpts, WithClientAssertionJWT(assertion)) - //} + if len(rp.ClientKey()) > 0 { + assertion, err := oidc.GenerateJWTProfileToken(oidc.NewJWTProfileAssertion(rp.OAuthConfig().ClientID, rp.ClientKeyID(), []string{"http://localhost:50002/oauth/v2"}, rp.ClientKey())) + if err != nil { + http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized) + return + } + codeOpts = append(codeOpts, WithClientAssertionJWT(assertion)) + } tokens, err := CodeExchange(r.Context(), params.Get("code"), rp, codeOpts...) if err != nil { http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) diff --git a/pkg/rp/resource_server.go b/pkg/rp/resource_server.go index e0488f1..c59097d 100644 --- a/pkg/rp/resource_server.go +++ b/pkg/rp/resource_server.go @@ -4,10 +4,11 @@ import ( "context" "errors" "net/http" + "net/url" + "time" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" - "golang.org/x/oauth2/jwt" "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/utils" @@ -16,6 +17,7 @@ import ( type ResourceServer interface { IntrospectionURL() string HttpClient() *http.Client + AuthFn() interface{} } type resourceServer struct { @@ -23,6 +25,32 @@ type resourceServer struct { tokenURL string introspectURL string httpClient *http.Client + authFn interface{} +} + +type jwtAccessTokenSource struct { + clientID string + audience []string + PrivateKey []byte + PrivateKeyID string +} + +func (j *jwtAccessTokenSource) Token() (*oauth2.Token, error) { + iat := time.Now() + exp := iat.Add(time.Hour) + assertion, err := GenerateJWTProfileToken(&oidc.JWTProfileAssertion{ + PrivateKeyID: j.PrivateKeyID, + PrivateKey: j.PrivateKey, + Issuer: j.clientID, + Subject: j.clientID, + Audience: j.audience, + Expiration: oidc.Time(exp), + IssuedAt: oidc.Time(iat), + }) + if err != nil { + return nil, err + } + return &oauth2.Token{AccessToken: assertion, TokenType: "Bearer", Expiry: exp}, nil } func (r *resourceServer) IntrospectionURL() string { @@ -33,8 +61,12 @@ func (r *resourceServer) HttpClient() *http.Client { return r.httpClient } +func (r *resourceServer) AuthFn() interface{} { + return r.authFn +} + func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option RSOption) (ResourceServer, error) { - authorizer := func(tokenURL string) func(ctx context.Context) *http.Client { + authorizer := func(tokenURL string) func(context.Context) *http.Client { return (&clientcredentials.Config{ ClientID: clientID, ClientSecret: clientSecret, @@ -44,20 +76,52 @@ func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, o return newResourceServer(issuer, authorizer, option) } func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...RSOption) (ResourceServer, error) { - authorizer := func(tokenURL string) func(ctx context.Context) *http.Client { - return (&jwt.Config{ - Email: clientID, - Subject: clientID, - PrivateKey: key, - PrivateKeyID: keyID, - Audience: issuer, - TokenURL: tokenURL, - }).Client + ts := &jwtAccessTokenSource{ + clientID: clientID, + PrivateKey: key, + PrivateKeyID: keyID, + audience: []string{issuer}, } + + //authorizer := func(tokenURL string) func(context.Context) *http.Client { + // return func(ctx context.Context) *http.Client { + // return oauth2.NewClient(ctx, oauth2.ReuseTokenSource(token, ts)) + // } + //} + authorizer := utils.FormAuthorization(func(values url.Values) { + token, err := ts.Token() + if err != nil { + //return nil, err + } + values.Set("client_assertion", token.AccessToken) + }) return newResourceServer(issuer, authorizer, options...) } -func newResourceServer(issuer string, authorizer func(tokenURL string) func(ctx context.Context) *http.Client, options ...RSOption) (*resourceServer, error) { +// +//func newResourceServer(issuer string, authorizer func(tokenURL string) func(ctx context.Context) *http.Client, options ...RSOption) (*resourceServer, error) { +// rp := &resourceServer{ +// issuer: issuer, +// httpClient: utils.DefaultHTTPClient, +// } +// for _, optFunc := range options { +// optFunc(rp) +// } +// if rp.introspectURL == "" || rp.tokenURL == "" { +// endpoints, err := Discover(rp.issuer, rp.httpClient) +// if err != nil { +// return nil, err +// } +// rp.tokenURL = endpoints.TokenURL +// rp.introspectURL = endpoints.IntrospectURL +// } +// if rp.introspectURL == "" || rp.tokenURL == "" { +// return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url") +// } +// //rp.httpClient = authorizer(rp.tokenURL)(context.WithValue(context.Background(), oauth2.HTTPClient, rp.HttpClient())) +// return rp, nil +//} +func newResourceServer(issuer string, authorizer interface{}, options ...RSOption) (*resourceServer, error) { rp := &resourceServer{ issuer: issuer, httpClient: utils.DefaultHTTPClient, @@ -76,7 +140,8 @@ func newResourceServer(issuer string, authorizer func(tokenURL string) func(ctx if rp.introspectURL == "" || rp.tokenURL == "" { return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url") } - rp.httpClient = authorizer(rp.tokenURL)(context.WithValue(context.Background(), oauth2.HTTPClient, rp.HttpClient())) + //rp.httpClient = authorizer(rp.tokenURL)(context.WithValue(context.Background(), oauth2.HTTPClient, rp.HttpClient())) + rp.authFn = authorizer return rp, nil } @@ -85,6 +150,7 @@ func NewResourceServerFromKeyFile(path string, options ...RSOption) (ResourceSer if err != nil { return nil, err } + c.Issuer = "http://localhost:50002/oauth/v2" return NewResourceServerJWTProfile(c.Issuer, c.ClientID, c.KeyID, []byte(c.Key), options...) } @@ -106,7 +172,7 @@ func WithStaticEndpoints(tokenURL, introspectURL string) RSOption { } func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.IntrospectionResponse, error) { - req, err := utils.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, encoder, nil) + req, err := utils.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, encoder, rp.AuthFn()) if err != nil { return nil, err } diff --git a/pkg/utils/http.go b/pkg/utils/http.go index fa51815..6f1b74d 100644 --- a/pkg/utils/http.go +++ b/pkg/utils/http.go @@ -42,6 +42,9 @@ func FormRequest(endpoint string, request interface{}, encoder Encoder, authFn i if fn, ok := authFn.(FormAuthorization); ok { fn(form) } + if fn, ok := authFn.(func(url.Values)); ok { + fn(form) + } body := strings.NewReader(form.Encode()) req, err := http.NewRequest("POST", endpoint, body) if err != nil { From 0ca2370d48b15b175141155f37673095de1729a9 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Thu, 11 Feb 2021 17:38:58 +0100 Subject: [PATCH 023/502] refactoring --- example/client/api/api.go | 9 +- example/client/app/app.go | 83 +-------- example/client/github/github.go | 10 +- example/client/service/service.go | 196 ++++++++++++++++++++++ pkg/client/client.go | 90 ++++++++++ pkg/client/jwt_profile.go | 30 ++++ pkg/{rp => client}/key.go | 8 +- pkg/client/profile/jwt_profile.go | 85 ++++++++++ pkg/{ => client}/rp/cli/cli.go | 8 +- pkg/{ => client}/rp/delegation.go | 0 pkg/{ => client}/rp/jwks.go | 0 pkg/{ => client}/rp/mock/generate.go | 0 pkg/{ => client}/rp/mock/verifier.mock.go | 10 +- pkg/{ => client}/rp/relaying_party.go | 196 ++++++++++------------ pkg/client/rp/tockenexchange.go | 27 +++ pkg/{ => client}/rp/verifier.go | 10 -- pkg/client/rs/resource_server.go | 123 ++++++++++++++ pkg/oidc/discovery.go | 1 + pkg/op/storage.go | 5 - pkg/op/token.go | 3 +- pkg/op/tokenrequest.go | 3 + pkg/op/userinfo.go | 3 +- pkg/rp/resource_server.go | 184 -------------------- pkg/rp/tockenexchange.go | 100 ----------- pkg/utils/key.go | 25 +++ 25 files changed, 698 insertions(+), 511 deletions(-) create mode 100644 example/client/service/service.go create mode 100644 pkg/client/client.go create mode 100644 pkg/client/jwt_profile.go rename pkg/{rp => client}/key.go (78%) create mode 100644 pkg/client/profile/jwt_profile.go rename pkg/{ => client}/rp/cli/cli.go (67%) rename pkg/{ => client}/rp/delegation.go (100%) rename pkg/{ => client}/rp/jwks.go (100%) rename pkg/{ => client}/rp/mock/generate.go (100%) rename pkg/{ => client}/rp/mock/verifier.mock.go (94%) rename pkg/{ => client}/rp/relaying_party.go (69%) create mode 100644 pkg/client/rp/tockenexchange.go rename pkg/{ => client}/rp/verifier.go (92%) create mode 100644 pkg/client/rs/resource_server.go delete mode 100644 pkg/rp/resource_server.go delete mode 100644 pkg/rp/tockenexchange.go create mode 100644 pkg/utils/key.go diff --git a/example/client/api/api.go b/example/client/api/api.go index e5345a8..a3ae85e 100644 --- a/example/client/api/api.go +++ b/example/client/api/api.go @@ -12,8 +12,8 @@ import ( "github.com/gorilla/mux" "github.com/sirupsen/logrus" + "github.com/caos/oidc/pkg/client/rs" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/rp" ) const ( @@ -25,8 +25,9 @@ const ( func main() { keyPath := os.Getenv("KEY") port := os.Getenv("PORT") + issuer := os.Getenv("ISSUER") - provider, err := rp.NewResourceServerFromKeyFile(keyPath) + provider, err := rs.NewResourceServerFromKeyFile(issuer, keyPath) if err != nil { logrus.Fatalf("error creating provider %s", err.Error()) } @@ -46,7 +47,7 @@ func main() { if !ok { return } - resp, err := rp.Introspect(r.Context(), provider, token) + resp, err := rs.Introspect(r.Context(), provider, token) if err != nil { http.Error(w, err.Error(), http.StatusForbidden) return @@ -67,7 +68,7 @@ func main() { if !ok { return } - resp, err := rp.Introspect(r.Context(), provider, token) + resp, err := rs.Introspect(r.Context(), provider, token) if err != nil { http.Error(w, err.Error(), http.StatusForbidden) return diff --git a/example/client/app/app.go b/example/client/app/app.go index c3bf9e0..e3ddd15 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -1,11 +1,8 @@ package main import ( - "context" "encoding/json" "fmt" - "html/template" - "io/ioutil" "net/http" "os" "strings" @@ -14,8 +11,8 @@ import ( "github.com/google/uuid" "github.com/sirupsen/logrus" + "github.com/caos/oidc/pkg/client/rp" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/rp" "github.com/caos/oidc/pkg/utils" ) @@ -32,8 +29,6 @@ func main() { port := os.Getenv("PORT") scopes := strings.Split(os.Getenv("SCOPES"), " ") - ctx := context.Background() - redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath) cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) @@ -48,7 +43,7 @@ func main() { options = append(options, rp.WithClientKey(keyPath)) } - provider, err := rp.NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...) + provider, err := rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...) if err != nil { logrus.Fatalf("error creating provider %s", err.Error()) } @@ -81,80 +76,6 @@ func main() { //with the returned tokens from the token endpoint http.Handle(callbackPath, rp.CodeExchangeHandler(marshal, provider)) - http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { - tokens, err := rp.ClientCredentials(ctx, provider, "scope") - if err != nil { - http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) - return - } - - data, err := json.Marshal(tokens) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Write(data) - }) - - http.HandleFunc("/jwt-profile", func(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - tpl := ` - - - - - Login - - -
- - - -
- - ` - t, err := template.New("login").Parse(tpl) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - err = t.Execute(w, nil) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - } else { - err := r.ParseMultipartForm(4 << 10) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - file, handler, err := r.FormFile("key") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer file.Close() - - key, err := ioutil.ReadAll(file) - fmt.Println(handler.Header) - assertion, err := oidc.NewJWTProfileAssertionFromFileData(key, []string{issuer}) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - token, err := rp.JWTProfileAssertionExchange(ctx, assertion, scopes, provider) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - data, err := json.Marshal(token) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Write(data) - } - }) lis := fmt.Sprintf("127.0.0.1:%s", port) logrus.Infof("listening on http://%s/", lis) logrus.Fatal(http.ListenAndServe("127.0.0.1:"+port, nil)) diff --git a/example/client/github/github.go b/example/client/github/github.go index c136091..f39c40b 100644 --- a/example/client/github/github.go +++ b/example/client/github/github.go @@ -10,8 +10,8 @@ import ( "golang.org/x/oauth2" githubOAuth "golang.org/x/oauth2/github" - "github.com/caos/oidc/pkg/rp" - "github.com/caos/oidc/pkg/rp/cli" + "github.com/caos/oidc/pkg/client/rp" + "github.com/caos/oidc/pkg/client/rp/cli" "github.com/caos/oidc/pkg/utils" ) @@ -35,7 +35,7 @@ func main() { ctx := context.Background() cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) - relayingParty, err := rp.NewRelayingPartyOAuth(rpConfig, rp.WithCookieHandler(cookieHandler)) + relyingParty, err := rp.NewRelyingPartyOAuth(rpConfig, rp.WithCookieHandler(cookieHandler)) if err != nil { fmt.Printf("error creating relaying party: %v", err) return @@ -43,9 +43,9 @@ func main() { state := func() string { return uuid.New().String() } - token := cli.CodeFlow(relayingParty, callbackPath, port, state) + token := cli.CodeFlow(relyingParty, callbackPath, port, state) - client := github.NewClient(relayingParty.OAuthConfig().Client(ctx, token.Token)) + client := github.NewClient(relyingParty.OAuthConfig().Client(ctx, token.Token)) _, _, err = client.Users.Get(ctx, "") if err != nil { diff --git a/example/client/service/service.go b/example/client/service/service.go new file mode 100644 index 0000000..95227d0 --- /dev/null +++ b/example/client/service/service.go @@ -0,0 +1,196 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "html/template" + "io/ioutil" + "net/http" + "os" + "strings" + + "github.com/sirupsen/logrus" + "golang.org/x/oauth2" + + "github.com/caos/oidc/pkg/client/profile" +) + +var ( + client *http.Client = http.DefaultClient +) + +func main() { + //keyPath := os.Getenv("KEY_PATH") + issuer := os.Getenv("ISSUER") + port := os.Getenv("PORT") + scopes := strings.Split(os.Getenv("SCOPES"), " ") + //testURL := os.Getenv("TEST_URL") + + //if keyPath != "" { + // ts, err := rp.NewJWTProfileTokenSourceFromFile(issuer, keyPath, scopes) + // if err != nil { + // logrus.Fatalf("error creating token source %s", err.Error()) + // } + // //client = oauth2.NewClient(context.Background(), ts) + // resp, err := callExampleEndpoint(client, testURL) + // if err != nil { + // logrus.Fatalf("error response from test url: %s", err.Error()) + // } + // fmt.Println(resp) + //} + + http.HandleFunc("/jwt-profile", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + tpl := ` + + + + + Login + + +
+ + + +
+ + ` + t, err := template.New("login").Parse(tpl) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + err = t.Execute(w, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } else { + err := r.ParseMultipartForm(4 << 10) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + file, _, err := r.FormFile("key") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer file.Close() + + key, err := ioutil.ReadAll(file) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + ts, err := profile.NewJWTProfileTokenSourceFromKeyFile(issuer, key, scopes) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + client = oauth2.NewClient(context.Background(), ts) + token, err := ts.Token() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + //assertion, err := oidc.NewJWTProfileAssertionFromFileData(key, []string{issuer}) + //if err != nil { + // http.Error(w, err.Error(), http.StatusInternalServerError) + // return + //} + //token, err := rp.JWTProfileAssertionExchange(ctx, assertion, scopes, provider) + //if err != nil { + // http.Error(w, err.Error(), http.StatusInternalServerError) + // return + //} + data, err := json.Marshal(token) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write(data) + } + }) + + http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { + tpl := ` + + + + + Test + + +
+ + + +
+ {{if .URL}} +

+ Result for {{.URL}}: {{.Response}} +

+ {{end}} + + ` + err := r.ParseForm() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + testURL := r.Form.Get("url") + var data struct { + URL string + Response interface{} + } + if testURL != "" { + data.URL = testURL + data.Response, err = callExampleEndpoint(client, testURL) + if err != nil { + data.Response = err + } + } + t, err := template.New("login").Parse(tpl) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + err = t.Execute(w, data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + }) + lis := fmt.Sprintf("127.0.0.1:%s", port) + logrus.Infof("listening on http://%s/", lis) + logrus.Fatal(http.ListenAndServe("127.0.0.1:"+port, nil)) +} + +func callExampleEndpoint(client *http.Client, testURL string) (interface{}, error) { + req, err := http.NewRequest("GET", testURL, nil) + if err != nil { + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("http status not ok: %s %s", resp.Status, body) + } + + if strings.HasPrefix(resp.Header.Get("content-type"), "text/plain") { + return string(body), nil + } + return body, err +} diff --git a/pkg/client/client.go b/pkg/client/client.go new file mode 100644 index 0000000..b2b815e --- /dev/null +++ b/pkg/client/client.go @@ -0,0 +1,90 @@ +package client + +import ( + "net/http" + "reflect" + "strings" + "time" + + "github.com/gorilla/schema" + "golang.org/x/oauth2" + "gopkg.in/square/go-jose.v2" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +var ( + Encoder = func() utils.Encoder { + e := schema.NewEncoder() + e.RegisterEncoder(oidc.Scopes{}, func(value reflect.Value) string { + return value.Interface().(oidc.Scopes).Encode() + }) + return e + }() +) + +//Discover calls the discovery endpoint of the provided issuer and returns its configuration +func Discover(issuer string, httpClient *http.Client) (*oidc.DiscoveryConfiguration, error) { + wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint + req, err := http.NewRequest("GET", wellKnown, nil) + if err != nil { + return nil, err + } + discoveryConfig := new(oidc.DiscoveryConfiguration) + err = utils.HttpRequest(httpClient, req, &discoveryConfig) + if err != nil { + return nil, err + } + return discoveryConfig, nil +} + +type tokenEndpointCaller interface { + TokenEndpoint() string + HttpClient() *http.Client +} + +func CallTokenEndpoint(request interface{}, caller tokenEndpointCaller) (newToken *oauth2.Token, err error) { + return callTokenEndpoint(request, nil, caller) +} + +func callTokenEndpoint(request interface{}, authFn interface{}, caller tokenEndpointCaller) (newToken *oauth2.Token, err error) { + req, err := utils.FormRequest(caller.TokenEndpoint(), request, Encoder, authFn) + if err != nil { + return nil, err + } + tokenRes := new(oidc.AccessTokenResponse) + if err := utils.HttpRequest(caller.HttpClient(), req, &tokenRes); err != nil { + return nil, err + } + return &oauth2.Token{ + AccessToken: tokenRes.AccessToken, + TokenType: tokenRes.TokenType, + RefreshToken: tokenRes.RefreshToken, + Expiry: time.Now().UTC().Add(time.Duration(tokenRes.ExpiresIn) * time.Second), + }, nil +} + +func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) { + privateKey, err := utils.BytesToPrivateKey(key) + if err != nil { + return nil, err + } + signingKey := jose.SigningKey{ + Algorithm: jose.RS256, + Key: &jose.JSONWebKey{Key: privateKey, KeyID: keyID}, + } + return jose.NewSigner(signingKey, &jose.SignerOptions{}) +} + +func SignedJWTProfileAssertion(clientID string, audience []string, expiration time.Duration, signer jose.Signer) (string, error) { + iat := time.Now() + exp := iat.Add(expiration) + return utils.Sign(&oidc.JWTTokenRequest{ + Issuer: clientID, + Subject: clientID, + Audience: audience, + ExpiresAt: oidc.Time(exp), + IssuedAt: oidc.Time(iat), + }, signer) +} diff --git a/pkg/client/jwt_profile.go b/pkg/client/jwt_profile.go new file mode 100644 index 0000000..8095588 --- /dev/null +++ b/pkg/client/jwt_profile.go @@ -0,0 +1,30 @@ +package client + +import ( + "context" + "net/url" + + "golang.org/x/oauth2" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +//JWTProfileExchange handles the oauth2 jwt profile exchange +func JWTProfileExchange(ctx context.Context, jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, caller tokenEndpointCaller) (*oauth2.Token, error) { + return CallTokenEndpoint(jwtProfileGrantRequest, caller) +} + +func ClientAssertionCodeOptions(assertion string) []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam("client_assertion", assertion), + oauth2.SetAuthURLParam("client_assertion_type", oidc.ClientAssertionTypeJWTAssertion), + } +} + +func ClientAssertionFormAuthorization(assertion string) utils.FormAuthorization { + return func(values url.Values) { + values.Set("client_assertion", assertion) + values.Set("client_assertion_type", oidc.ClientAssertionTypeJWTAssertion) + } +} diff --git a/pkg/rp/key.go b/pkg/client/key.go similarity index 78% rename from pkg/rp/key.go rename to pkg/client/key.go index 26d8bf5..f89a2b4 100644 --- a/pkg/rp/key.go +++ b/pkg/client/key.go @@ -1,4 +1,4 @@ -package rp +package client import ( "encoding/json" @@ -14,7 +14,7 @@ type keyFile struct { Type string `json:"type"` // serviceaccount or application KeyID string `json:"keyId"` Key string `json:"key"` - Issuer string `json:"issuer"` + Issuer string `json:"issuer"` //not yet in file //serviceaccount UserID string `json:"userId"` @@ -28,6 +28,10 @@ func ConfigFromKeyFile(path string) (*keyFile, error) { if err != nil { return nil, err } + return ConfigFromKeyFileData(data) +} + +func ConfigFromKeyFileData(data []byte) (*keyFile, error) { var f keyFile if err := json.Unmarshal(data, &f); err != nil { return nil, err diff --git a/pkg/client/profile/jwt_profile.go b/pkg/client/profile/jwt_profile.go new file mode 100644 index 0000000..d60a2f8 --- /dev/null +++ b/pkg/client/profile/jwt_profile.go @@ -0,0 +1,85 @@ +package profile + +import ( + "net/http" + "time" + + "golang.org/x/oauth2" + "gopkg.in/square/go-jose.v2" + + "github.com/caos/oidc/pkg/client" + "github.com/caos/oidc/pkg/oidc" +) + +//jwtProfileTokenSource implement the oauth2.TokenSource +//it will request a token using the OAuth2 JWT Profile Grant +//therefore sending an `assertion` by singing a JWT with the provided private key +type jwtProfileTokenSource struct { + clientID string + audience []string + signer jose.Signer + scopes []string + httpClient *http.Client + tokenEndpoint string +} + +func NewJWTProfileTokenSourceFromKeyFile(issuer string, data []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) { + keyData, err := client.ConfigFromKeyFileData(data) + if err != nil { + return nil, err + } + return NewJWTProfileTokenSource(issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...) +} + +func NewJWTProfileTokenSource(issuer, clientID, keyID string, key []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) { + signer, err := client.NewSignerFromPrivateKeyByte(key, keyID) + if err != nil { + return nil, err + } + source := &jwtProfileTokenSource{ + clientID: clientID, + audience: []string{issuer}, + signer: signer, + scopes: scopes, + httpClient: http.DefaultClient, + } + for _, opt := range options { + opt(source) + } + if source.tokenEndpoint == "" { + config, err := client.Discover(issuer, source.httpClient) + if err != nil { + return nil, err + } + source.tokenEndpoint = config.TokenEndpoint + } + return source, nil +} + +func WithHTTPClient(client *http.Client) func(*jwtProfileTokenSource) { + return func(source *jwtProfileTokenSource) { + source.httpClient = client + } +} + +func WithStaticTokenEndpoint(issuer, tokenEndpoint string) func(*jwtProfileTokenSource) { + return func(source *jwtProfileTokenSource) { + source.tokenEndpoint = tokenEndpoint + } +} + +func (j *jwtProfileTokenSource) TokenEndpoint() string { + return j.tokenEndpoint +} + +func (j *jwtProfileTokenSource) HttpClient() *http.Client { + return j.httpClient +} + +func (j *jwtProfileTokenSource) Token() (*oauth2.Token, error) { + assertion, err := client.SignedJWTProfileAssertion(j.clientID, j.audience, time.Hour, j.signer) + if err != nil { + return nil, err + } + return client.JWTProfileExchange(nil, oidc.NewJWTProfileGrantRequest(assertion, j.scopes...), j) +} diff --git a/pkg/rp/cli/cli.go b/pkg/client/rp/cli/cli.go similarity index 67% rename from pkg/rp/cli/cli.go rename to pkg/client/rp/cli/cli.go index 4b00ba0..6cbb364 100644 --- a/pkg/rp/cli/cli.go +++ b/pkg/client/rp/cli/cli.go @@ -4,8 +4,8 @@ import ( "context" "net/http" + "github.com/caos/oidc/pkg/client/rp" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/rp" "github.com/caos/oidc/pkg/utils" ) @@ -13,7 +13,7 @@ const ( loginPath = "/login" ) -func CodeFlow(relayingParty rp.RelayingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens { +func CodeFlow(relyingParty rp.RelyingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -24,8 +24,8 @@ func CodeFlow(relayingParty rp.RelayingParty, callbackPath, port string, statePr msg = msg + "

You are authenticated and can now return to the CLI.

" w.Write([]byte(msg)) } - http.Handle(loginPath, rp.AuthURLHandler(stateProvider, relayingParty)) - http.Handle(callbackPath, rp.CodeExchangeHandler(callback, relayingParty)) + http.Handle(loginPath, rp.AuthURLHandler(stateProvider, relyingParty)) + http.Handle(callbackPath, rp.CodeExchangeHandler(callback, relyingParty)) utils.StartServer(ctx, port) diff --git a/pkg/rp/delegation.go b/pkg/client/rp/delegation.go similarity index 100% rename from pkg/rp/delegation.go rename to pkg/client/rp/delegation.go diff --git a/pkg/rp/jwks.go b/pkg/client/rp/jwks.go similarity index 100% rename from pkg/rp/jwks.go rename to pkg/client/rp/jwks.go diff --git a/pkg/rp/mock/generate.go b/pkg/client/rp/mock/generate.go similarity index 100% rename from pkg/rp/mock/generate.go rename to pkg/client/rp/mock/generate.go diff --git a/pkg/rp/mock/verifier.mock.go b/pkg/client/rp/mock/verifier.mock.go similarity index 94% rename from pkg/rp/mock/verifier.mock.go rename to pkg/client/rp/mock/verifier.mock.go index acd7d77..08cf77f 100644 --- a/pkg/rp/mock/verifier.mock.go +++ b/pkg/client/rp/mock/verifier.mock.go @@ -5,10 +5,12 @@ package mock import ( - context "context" - oidc "github.com/caos/oidc/pkg/oidc" - gomock "github.com/golang/mock/gomock" - reflect "reflect" + "context" + "reflect" + + "github.com/golang/mock/gomock" + + "github.com/caos/oidc/pkg/oidc" ) // MockVerifier is a mock of Verifier interface diff --git a/pkg/rp/relaying_party.go b/pkg/client/rp/relaying_party.go similarity index 69% rename from pkg/rp/relaying_party.go rename to pkg/client/rp/relaying_party.go index 3260657..528f554 100644 --- a/pkg/rp/relaying_party.go +++ b/pkg/client/rp/relaying_party.go @@ -4,43 +4,32 @@ import ( "context" "errors" "net/http" - "net/url" - "reflect" "strings" "time" "github.com/google/uuid" - "github.com/gorilla/schema" - - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/oidc/grants" - "github.com/caos/oidc/pkg/utils" - "golang.org/x/oauth2" + "gopkg.in/square/go-jose.v2" + + "github.com/caos/oidc/pkg/client" + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" ) const ( - idTokenKey = "id_token" - stateParam = "state" - pkceCode = "pkce" - jwtProfileKey = "urn:ietf:params:oauth:grant-type:jwt-bearer" + idTokenKey = "id_token" + stateParam = "state" + pkceCode = "pkce" ) -var ( - encoder = func() utils.Encoder { - e := schema.NewEncoder() - e.RegisterEncoder(oidc.Scopes{}, func(value reflect.Value) string { - return value.Interface().(oidc.Scopes).Encode() - }) - return e - }() -) - -//RelayingParty declares the minimal interface for oidc clients -type RelayingParty interface { +//RelyingParty declares the minimal interface for oidc clients +type RelyingParty interface { //OAuthConfig returns the oauth2 Config OAuthConfig() *oauth2.Config + //Issuer returns the issuer of the oidc config + Issuer() string + //IsPKCE returns if authorization is done using `Authorization Code Flow with Proof Key for Code Exchange (PKCE)` IsPKCE() bool @@ -53,13 +42,16 @@ type RelayingParty interface { //IsOAuth2Only specifies whether relaying party handles only oauth2 or oidc calls IsOAuth2Only() bool - ClientKey() []byte - ClientKeyID() string + //Signer is used if the relaying party uses the JWT Profile + Signer() jose.Signer + + //UserinfoEndpoint returns the userinfo + UserinfoEndpoint() string //IDTokenVerifier returns the verifier interface used for oidc id_token verification IDTokenVerifier() IDTokenVerifier - //ErrorHandler returns the handler used for callback errors + ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) } @@ -71,14 +63,12 @@ var ( } ) -type relayingParty struct { +type relyingParty struct { issuer string endpoints Endpoints oauthConfig *oauth2.Config oauth2Only bool pkce bool - clientKey []byte - clientKeyID string httpClient *http.Client cookieHandler *utils.CookieHandler @@ -86,72 +76,79 @@ type relayingParty struct { errorHandler func(http.ResponseWriter, *http.Request, string, string, string) idTokenVerifier IDTokenVerifier verifierOpts []VerifierOption + signer jose.Signer } -func (rp *relayingParty) OAuthConfig() *oauth2.Config { +func (rp *relyingParty) OAuthConfig() *oauth2.Config { return rp.oauthConfig } -func (rp *relayingParty) IsPKCE() bool { +func (rp *relyingParty) Issuer() string { + return rp.issuer +} + +func (rp *relyingParty) IsPKCE() bool { return rp.pkce } -func (rp *relayingParty) CookieHandler() *utils.CookieHandler { +func (rp *relyingParty) CookieHandler() *utils.CookieHandler { return rp.cookieHandler } -func (rp *relayingParty) HttpClient() *http.Client { +func (rp *relyingParty) HttpClient() *http.Client { return rp.httpClient } -func (rp *relayingParty) IsOAuth2Only() bool { +func (rp *relyingParty) IsOAuth2Only() bool { return rp.oauth2Only } -func (rp *relayingParty) ClientKey() []byte { - return rp.clientKey +func (rp *relyingParty) Signer() jose.Signer { + return rp.signer } -func (rp *relayingParty) ClientKeyID() string { - return rp.clientKeyID +func (rp *relyingParty) UserinfoEndpoint() string { + return rp.endpoints.UserinfoURL } -func (rp *relayingParty) IDTokenVerifier() IDTokenVerifier { +func (rp *relyingParty) IDTokenVerifier() IDTokenVerifier { if rp.idTokenVerifier == nil { rp.idTokenVerifier = NewIDTokenVerifier(rp.issuer, rp.oauthConfig.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...) } return rp.idTokenVerifier } -func (rp *relayingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) { +func (rp *relyingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) { if rp.errorHandler == nil { rp.errorHandler = DefaultErrorHandler } return rp.errorHandler } -//NewRelayingPartyOAuth creates an (OAuth2) RelayingParty with the given +//NewRelyingPartyOAuth creates an (OAuth2) RelyingParty with the given //OAuth2 Config and possible configOptions //it will use the AuthURL and TokenURL set in config -func NewRelayingPartyOAuth(config *oauth2.Config, options ...Option) (RelayingParty, error) { - rp := &relayingParty{ +func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingParty, error) { + rp := &relyingParty{ oauthConfig: config, httpClient: utils.DefaultHTTPClient, oauth2Only: true, } for _, optFunc := range options { - optFunc(rp) + if err := optFunc(rp); err != nil { + return nil, err + } } return rp, nil } -//NewRelayingPartyOIDC creates an (OIDC) RelayingParty with the given +//NewRelyingPartyOIDC creates an (OIDC) RelyingParty with the given //issuer, clientID, clientSecret, redirectURI, scopes and possible configOptions //it will run discovery on the provided issuer and use the found endpoints -func NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelayingParty, error) { - rp := &relayingParty{ +func NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelyingParty, error) { + rp := &relyingParty{ issuer: issuer, oauthConfig: &oauth2.Config{ ClientID: clientID, @@ -164,7 +161,9 @@ func NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, sc } for _, optFunc := range options { - optFunc(rp) + if err := optFunc(rp); err != nil { + return nil, err + } } endpoints, err := Discover(rp.issuer, rp.httpClient) @@ -178,12 +177,13 @@ func NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, sc } //DefaultRPOpts is the type for providing dynamic options to the DefaultRP -type Option func(*relayingParty) +type Option func(*relyingParty) error //WithCookieHandler set a `CookieHandler` for securing the various redirects func WithCookieHandler(cookieHandler *utils.CookieHandler) Option { - return func(rp *relayingParty) { + return func(rp *relyingParty) error { rp.cookieHandler = cookieHandler + return nil } } @@ -191,40 +191,49 @@ func WithCookieHandler(cookieHandler *utils.CookieHandler) Option { //it also sets a `CookieHandler` for securing the various redirects //and exchanging the code challenge func WithPKCE(cookieHandler *utils.CookieHandler) Option { - return func(rp *relayingParty) { + return func(rp *relyingParty) error { rp.pkce = true rp.cookieHandler = cookieHandler + return nil } } //WithHTTPClient provides the ability to set an http client to be used for the relaying party and verifier func WithHTTPClient(client *http.Client) Option { - return func(rp *relayingParty) { + return func(rp *relyingParty) error { rp.httpClient = client + return nil } } func WithErrorHandler(errorHandler ErrorHandler) Option { - return func(rp *relayingParty) { + return func(rp *relyingParty) error { rp.errorHandler = errorHandler + return nil } } func WithVerifierOpts(opts ...VerifierOption) Option { - return func(rp *relayingParty) { + return func(rp *relyingParty) error { rp.verifierOpts = opts + return nil } } func WithClientKey(path string) Option { - return func(rp *relayingParty) { - config, _ := ConfigFromKeyFile(path) - rp.clientKey = []byte(config.Key) - rp.clientKeyID = config.KeyID + return func(rp *relyingParty) error { + config, err := client.ConfigFromKeyFile(path) + if err != nil { + return err + } + rp.signer, err = client.NewSignerFromPrivateKeyByte([]byte(config.Key), config.KeyID) + return err } } //Discover calls the discovery endpoint of the provided issuer and returns the found endpoints +// +//deprecated: use client.Discover func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint req, err := http.NewRequest("GET", wellKnown, nil) @@ -241,7 +250,7 @@ func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { //AuthURL returns the auth request url //(wrapping the oauth2 `AuthCodeURL`) -func AuthURL(state string, rp RelayingParty, opts ...AuthURLOpt) string { +func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string { authOpts := make([]oauth2.AuthCodeOption, 0) for _, opt := range opts { authOpts = append(authOpts, opt()...) @@ -251,7 +260,7 @@ func AuthURL(state string, rp RelayingParty, opts ...AuthURLOpt) string { //AuthURLHandler extends the `AuthURL` method with a http redirect handler //including handling setting cookie for secure `state` transfer -func AuthURLHandler(stateFn func() string, rp RelayingParty) http.HandlerFunc { +func AuthURLHandler(stateFn func() string, rp RelyingParty) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { opts := make([]AuthURLOpt, 0) state := stateFn() @@ -272,7 +281,7 @@ func AuthURLHandler(stateFn func() string, rp RelayingParty) http.HandlerFunc { } //GenerateAndStoreCodeChallenge generates a PKCE code challenge and stores its verifier into a secure cookie -func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelayingParty) (string, error) { +func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (string, error) { codeVerifier := uuid.New().String() if err := rp.CookieHandler().SetCookie(w, pkceCode, codeVerifier); err != nil { return "", err @@ -282,7 +291,7 @@ func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelayingParty) (str //CodeExchange handles the oauth2 code exchange, extracting and validating the id_token //returning it parsed together with the oauth2 tokens (access, refresh) -func CodeExchange(ctx context.Context, code string, rp RelayingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) { +func CodeExchange(ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) { ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient()) codeOpts := make([]oauth2.AuthCodeOption, 0) for _, opt := range opts { @@ -314,7 +323,7 @@ func CodeExchange(ctx context.Context, code string, rp RelayingParty, opts ...Co //CodeExchangeHandler extends the `CodeExchange` method with a http handler //including cookie handling for secure `state` transfer //and optional PKCE code verifier checking -func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string), rp RelayingParty) http.HandlerFunc { +func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string), rp RelyingParty) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { state, err := tryReadStateCookie(w, r, rp) if err != nil { @@ -335,8 +344,8 @@ func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc } codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier)) } - if len(rp.ClientKey()) > 0 { - assertion, err := oidc.GenerateJWTProfileToken(oidc.NewJWTProfileAssertion(rp.OAuthConfig().ClientID, rp.ClientKeyID(), []string{"http://localhost:50002/oauth/v2"}, rp.ClientKey())) + if rp.Signer() != nil { + assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer()}, time.Hour, rp.Signer()) if err != nil { http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized) return @@ -352,51 +361,21 @@ func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc } } -//ClientCredentials is the `RelayingParty` interface implementation -//handling the oauth2 client credentials grant -func ClientCredentials(ctx context.Context, rp RelayingParty, scopes ...string) (newToken *oauth2.Token, err error) { - return CallTokenEndpointAuthorized(grants.ClientCredentialsGrantBasic(scopes...), rp) -} - -func CallTokenEndpointAuthorized(request interface{}, rp RelayingParty) (newToken *oauth2.Token, err error) { - config := rp.OAuthConfig() - var fn interface{} = utils.AuthorizeBasic(config.ClientID, config.ClientSecret) - if config.Endpoint.AuthStyle == oauth2.AuthStyleInParams { - fn = func(form url.Values) { - form.Set("client_id", config.ClientID) - form.Set("client_secret", config.ClientSecret) - } - } - return callTokenEndpoint(request, fn, rp) -} - -func CallTokenEndpoint(request interface{}, rp RelayingParty) (newToken *oauth2.Token, err error) { - return callTokenEndpoint(request, nil, rp) -} - -func callTokenEndpoint(request interface{}, authFn interface{}, rp RelayingParty) (newToken *oauth2.Token, err error) { - req, err := utils.FormRequest(rp.OAuthConfig().Endpoint.TokenURL, request, encoder, authFn) +//Userinfo will call the OIDC Userinfo Endpoint with the provided token +func Userinfo(token string, rp RelyingParty) (oidc.UserInfo, error) { + req, err := http.NewRequest("GET", rp.UserinfoEndpoint(), nil) if err != nil { return nil, err } - var tokenRes struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - ExpiresIn int64 `json:"expires_in"` - RefreshToken string `json:"refresh_token"` - } - if err := utils.HttpRequest(rp.HttpClient(), req, &tokenRes); err != nil { + req.Header.Set("authorization", token) + userinfo := oidc.NewUserInfo() + if err := utils.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil { return nil, err } - return &oauth2.Token{ - AccessToken: tokenRes.AccessToken, - TokenType: tokenRes.TokenType, - RefreshToken: tokenRes.RefreshToken, - Expiry: time.Now().UTC().Add(time.Duration(tokenRes.ExpiresIn) * time.Second), - }, nil + return userinfo, nil } -func trySetStateCookie(w http.ResponseWriter, state string, rp RelayingParty) error { +func trySetStateCookie(w http.ResponseWriter, state string, rp RelyingParty) error { if rp.CookieHandler() != nil { if err := rp.CookieHandler().SetCookie(w, stateParam, state); err != nil { return err @@ -405,7 +384,7 @@ func trySetStateCookie(w http.ResponseWriter, state string, rp RelayingParty) er return nil } -func tryReadStateCookie(w http.ResponseWriter, r *http.Request, rp RelayingParty) (state string, err error) { +func tryReadStateCookie(w http.ResponseWriter, r *http.Request, rp RelyingParty) (state string, err error) { if rp.CookieHandler() == nil { return r.FormValue(stateParam), nil } @@ -417,7 +396,7 @@ func tryReadStateCookie(w http.ResponseWriter, r *http.Request, rp RelayingParty return state, nil } -type OptionFunc func(RelayingParty) +type OptionFunc func(RelyingParty) type Endpoints struct { oauth2.Endpoint @@ -472,9 +451,6 @@ func WithCodeVerifier(codeVerifier string) CodeExchangeOpt { //WithClientAssertionJWT sets the `client_assertion` param in the token request func WithClientAssertionJWT(clientAssertion string) CodeExchangeOpt { return func() []oauth2.AuthCodeOption { - return []oauth2.AuthCodeOption{ - oauth2.SetAuthURLParam("client_assertion", clientAssertion), - oauth2.SetAuthURLParam("client_assertion_type", oidc.ClientAssertionTypeJWTAssertion), - } + return client.ClientAssertionCodeOptions(clientAssertion) } } diff --git a/pkg/client/rp/tockenexchange.go b/pkg/client/rp/tockenexchange.go new file mode 100644 index 0000000..d5056ae --- /dev/null +++ b/pkg/client/rp/tockenexchange.go @@ -0,0 +1,27 @@ +package rp + +import ( + "context" + + "golang.org/x/oauth2" + + "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" +) + +//TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange` +type TokenExchangeRP interface { + RelyingParty + + //TokenExchange implement the `Token Exchange Grant` exchanging some token for an other + TokenExchange(context.Context, *tokenexchange.TokenExchangeRequest) (*oauth2.Token, error) +} + +//DelegationTokenExchangeRP extends the `TokenExchangeRP` interface +//for the specific `delegation token` request +type DelegationTokenExchangeRP interface { + TokenExchangeRP + + //DelegationTokenExchange implement the `Token Exchange Grant` + //providing an access token in request for a `delegation` token for a given resource / audience + DelegationTokenExchange(context.Context, string, ...tokenexchange.TokenExchangeOption) (*oauth2.Token, error) +} diff --git a/pkg/rp/verifier.go b/pkg/client/rp/verifier.go similarity index 92% rename from pkg/rp/verifier.go rename to pkg/client/rp/verifier.go index a156f6d..1f45ca8 100644 --- a/pkg/rp/verifier.go +++ b/pkg/client/rp/verifier.go @@ -214,13 +214,3 @@ func (i *idTokenVerifier) ACR() oidc.ACRVerifier { func (i *idTokenVerifier) MaxAge() time.Duration { return i.maxAge } - -//deprecated: Use IDTokenVerifier (or oidc.Verifier) -type Verifier interface { - - //Verify checks the access_token and id_token and returns the `id token claims` - Verify(ctx context.Context, accessToken, idTokenString string) (*oidc.IDTokenClaims, error) - - //VerifyIDToken checks the id_token only and returns its `id token claims` - VerifyIDToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error) -} diff --git a/pkg/client/rs/resource_server.go b/pkg/client/rs/resource_server.go new file mode 100644 index 0000000..f5dbe69 --- /dev/null +++ b/pkg/client/rs/resource_server.go @@ -0,0 +1,123 @@ +package rs + +import ( + "context" + "errors" + "net/http" + "time" + + "github.com/caos/oidc/pkg/client" + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +type ResourceServer interface { + IntrospectionURL() string + HttpClient() *http.Client + AuthFn() (interface{}, error) +} + +type resourceServer struct { + issuer string + tokenURL string + introspectURL string + httpClient *http.Client + authFn func() (interface{}, error) +} + +func (r *resourceServer) IntrospectionURL() string { + return r.introspectURL +} + +func (r *resourceServer) HttpClient() *http.Client { + return r.httpClient +} + +func (r *resourceServer) AuthFn() (interface{}, error) { + return r.authFn() +} + +func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option Option) (ResourceServer, error) { + authorizer := func() (interface{}, error) { + return utils.AuthorizeBasic(clientID, clientSecret), nil + } + return newResourceServer(issuer, authorizer, option) +} +func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...Option) (ResourceServer, error) { + signer, err := client.NewSignerFromPrivateKeyByte(key, keyID) + if err != nil { + return nil, err + } + authorizer := func() (interface{}, error) { + assertion, err := client.SignedJWTProfileAssertion(clientID, []string{issuer}, time.Hour, signer) + if err != nil { + return nil, err + } + return client.ClientAssertionFormAuthorization(assertion), nil + } + return newResourceServer(issuer, authorizer, options...) +} + +func newResourceServer(issuer string, authorizer func() (interface{}, error), options ...Option) (*resourceServer, error) { + rs := &resourceServer{ + issuer: issuer, + httpClient: utils.DefaultHTTPClient, + } + for _, optFunc := range options { + optFunc(rs) + } + if rs.introspectURL == "" || rs.tokenURL == "" { + config, err := client.Discover(rs.issuer, rs.httpClient) + if err != nil { + return nil, err + } + rs.tokenURL = config.TokenEndpoint + rs.introspectURL = config.IntrospectionEndpoint + } + if rs.introspectURL == "" || rs.tokenURL == "" { + return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url") + } + rs.authFn = authorizer + return rs, nil +} + +func NewResourceServerFromKeyFile(issuer, path string, options ...Option) (ResourceServer, error) { + c, err := client.ConfigFromKeyFile(path) + if err != nil { + return nil, err + } + return NewResourceServerJWTProfile(issuer, c.ClientID, c.KeyID, []byte(c.Key), options...) +} + +type Option func(*resourceServer) + +//WithClient provides the ability to set an http client to be used for the resource server +func WithClient(client *http.Client) Option { + return func(server *resourceServer) { + server.httpClient = client + } +} + +//WithStaticEndpoints provides the ability to set static token and introspect URL +func WithStaticEndpoints(tokenURL, introspectURL string) Option { + return func(server *resourceServer) { + server.tokenURL = tokenURL + server.introspectURL = introspectURL + } +} + +func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.IntrospectionResponse, error) { + authFn, err := rp.AuthFn() + if err != nil { + return nil, err + } + req, err := utils.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, client.Encoder, authFn) + if err != nil { + return nil, err + } + resp := oidc.NewIntrospectionResponse() + if err := utils.HttpRequest(rp.HttpClient(), req, resp); err != nil { + return nil, err + } + return resp, nil +} diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go index 104078c..ef1d65e 100644 --- a/pkg/oidc/discovery.go +++ b/pkg/oidc/discovery.go @@ -42,6 +42,7 @@ type DiscoveryConfiguration struct { DisplayValuesSupported []Display `json:"display_values_supported,omitempty"` ClaimTypesSupported []string `json:"claim_types_supported,omitempty"` ClaimsSupported []string `json:"claims_supported,omitempty"` + ClaimsParameterSupported bool `json:"claims_parameter_supported,omitempty"` CodeChallengeMethodsSupported []CodeChallengeMethod `json:"code_challenge_methods_supported,omitempty"` ServiceDocumentation string `json:"service_documentation,omitempty"` ClaimsLocalesSupported []language.Tag `json:"claims_locales_supported,omitempty"` diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 967a24c..33ed6ce 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -34,11 +34,6 @@ type OPStorage interface { GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) - - //deprecated: use GetUserinfoFromScopes instead - GetUserinfoFromScopes(ctx context.Context, userID, clientID string, scopes []string) (oidc.UserInfo, error) - //deprecated: use SetUserinfoFromToken instead - GetUserinfoFromToken(ctx context.Context, tokenID, subject, origin string) (oidc.UserInfo, error) } type Storage interface { diff --git a/pkg/op/token.go b/pkg/op/token.go index 5331d44..334bec9 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -114,7 +114,8 @@ func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, vali } } if len(scopes) > 0 { - userInfo, err := storage.GetUserinfoFromScopes(ctx, authReq.GetSubject(), authReq.GetClientID(), scopes) + userInfo := oidc.NewUserInfo() + err := storage.SetUserinfoFromScopes(ctx, userInfo, authReq.GetSubject(), authReq.GetClientID(), scopes) if err != nil { return "", err } diff --git a/pkg/op/tokenrequest.go b/pkg/op/tokenrequest.go index e0729bf..b51d2c8 100644 --- a/pkg/op/tokenrequest.go +++ b/pkg/op/tokenrequest.go @@ -123,6 +123,9 @@ func AuthorizeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exc if err != nil { return nil, nil, err } + if client.AuthMethod() == oidc.AuthMethodPrivateKeyJWT { + return nil, nil, errors.New("invalid_grant") + } if client.AuthMethod() == oidc.AuthMethodNone { authReq, err := AuthorizeCodeChallenge(ctx, tokenReq, exchanger) return authReq, client, err diff --git a/pkg/op/userinfo.go b/pkg/op/userinfo.go index d951136..9abf378 100644 --- a/pkg/op/userinfo.go +++ b/pkg/op/userinfo.go @@ -34,7 +34,8 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP http.Error(w, "access token invalid", http.StatusUnauthorized) return } - info, err := userinfoProvider.Storage().GetUserinfoFromToken(r.Context(), tokenID, subject, r.Header.Get("origin")) + info := oidc.NewUserInfo() + err = userinfoProvider.Storage().SetUserinfoFromToken(r.Context(), info, tokenID, subject, r.Header.Get("origin")) if err != nil { w.WriteHeader(http.StatusForbidden) utils.MarshalJSON(w, err) diff --git a/pkg/rp/resource_server.go b/pkg/rp/resource_server.go deleted file mode 100644 index c59097d..0000000 --- a/pkg/rp/resource_server.go +++ /dev/null @@ -1,184 +0,0 @@ -package rp - -import ( - "context" - "errors" - "net/http" - "net/url" - "time" - - "golang.org/x/oauth2" - "golang.org/x/oauth2/clientcredentials" - - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" -) - -type ResourceServer interface { - IntrospectionURL() string - HttpClient() *http.Client - AuthFn() interface{} -} - -type resourceServer struct { - issuer string - tokenURL string - introspectURL string - httpClient *http.Client - authFn interface{} -} - -type jwtAccessTokenSource struct { - clientID string - audience []string - PrivateKey []byte - PrivateKeyID string -} - -func (j *jwtAccessTokenSource) Token() (*oauth2.Token, error) { - iat := time.Now() - exp := iat.Add(time.Hour) - assertion, err := GenerateJWTProfileToken(&oidc.JWTProfileAssertion{ - PrivateKeyID: j.PrivateKeyID, - PrivateKey: j.PrivateKey, - Issuer: j.clientID, - Subject: j.clientID, - Audience: j.audience, - Expiration: oidc.Time(exp), - IssuedAt: oidc.Time(iat), - }) - if err != nil { - return nil, err - } - return &oauth2.Token{AccessToken: assertion, TokenType: "Bearer", Expiry: exp}, nil -} - -func (r *resourceServer) IntrospectionURL() string { - return r.introspectURL -} - -func (r *resourceServer) HttpClient() *http.Client { - return r.httpClient -} - -func (r *resourceServer) AuthFn() interface{} { - return r.authFn -} - -func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option RSOption) (ResourceServer, error) { - authorizer := func(tokenURL string) func(context.Context) *http.Client { - return (&clientcredentials.Config{ - ClientID: clientID, - ClientSecret: clientSecret, - TokenURL: tokenURL, - }).Client - } - return newResourceServer(issuer, authorizer, option) -} -func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...RSOption) (ResourceServer, error) { - ts := &jwtAccessTokenSource{ - clientID: clientID, - PrivateKey: key, - PrivateKeyID: keyID, - audience: []string{issuer}, - } - - //authorizer := func(tokenURL string) func(context.Context) *http.Client { - // return func(ctx context.Context) *http.Client { - // return oauth2.NewClient(ctx, oauth2.ReuseTokenSource(token, ts)) - // } - //} - authorizer := utils.FormAuthorization(func(values url.Values) { - token, err := ts.Token() - if err != nil { - //return nil, err - } - values.Set("client_assertion", token.AccessToken) - }) - return newResourceServer(issuer, authorizer, options...) -} - -// -//func newResourceServer(issuer string, authorizer func(tokenURL string) func(ctx context.Context) *http.Client, options ...RSOption) (*resourceServer, error) { -// rp := &resourceServer{ -// issuer: issuer, -// httpClient: utils.DefaultHTTPClient, -// } -// for _, optFunc := range options { -// optFunc(rp) -// } -// if rp.introspectURL == "" || rp.tokenURL == "" { -// endpoints, err := Discover(rp.issuer, rp.httpClient) -// if err != nil { -// return nil, err -// } -// rp.tokenURL = endpoints.TokenURL -// rp.introspectURL = endpoints.IntrospectURL -// } -// if rp.introspectURL == "" || rp.tokenURL == "" { -// return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url") -// } -// //rp.httpClient = authorizer(rp.tokenURL)(context.WithValue(context.Background(), oauth2.HTTPClient, rp.HttpClient())) -// return rp, nil -//} -func newResourceServer(issuer string, authorizer interface{}, options ...RSOption) (*resourceServer, error) { - rp := &resourceServer{ - issuer: issuer, - httpClient: utils.DefaultHTTPClient, - } - for _, optFunc := range options { - optFunc(rp) - } - if rp.introspectURL == "" || rp.tokenURL == "" { - endpoints, err := Discover(rp.issuer, rp.httpClient) - if err != nil { - return nil, err - } - rp.tokenURL = endpoints.TokenURL - rp.introspectURL = endpoints.IntrospectURL - } - if rp.introspectURL == "" || rp.tokenURL == "" { - return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url") - } - //rp.httpClient = authorizer(rp.tokenURL)(context.WithValue(context.Background(), oauth2.HTTPClient, rp.HttpClient())) - rp.authFn = authorizer - return rp, nil -} - -func NewResourceServerFromKeyFile(path string, options ...RSOption) (ResourceServer, error) { - c, err := ConfigFromKeyFile(path) - if err != nil { - return nil, err - } - c.Issuer = "http://localhost:50002/oauth/v2" - return NewResourceServerJWTProfile(c.Issuer, c.ClientID, c.KeyID, []byte(c.Key), options...) -} - -type RSOption func(*resourceServer) - -//WithClient provides the ability to set an http client to be used for the resource server -func WithClient(client *http.Client) RSOption { - return func(server *resourceServer) { - server.httpClient = client - } -} - -//WithStaticEndpoints provides the ability to set static token and introspect URL -func WithStaticEndpoints(tokenURL, introspectURL string) RSOption { - return func(server *resourceServer) { - server.tokenURL = tokenURL - server.introspectURL = introspectURL - } -} - -func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.IntrospectionResponse, error) { - req, err := utils.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, encoder, rp.AuthFn()) - if err != nil { - return nil, err - } - resp := oidc.NewIntrospectionResponse() - if err := utils.HttpRequest(rp.HttpClient(), req, resp); err != nil { - return nil, err - } - return resp, nil -} diff --git a/pkg/rp/tockenexchange.go b/pkg/rp/tockenexchange.go deleted file mode 100644 index bdca44b..0000000 --- a/pkg/rp/tockenexchange.go +++ /dev/null @@ -1,100 +0,0 @@ -package rp - -import ( - "context" - "crypto/rsa" - "crypto/x509" - "encoding/json" - "encoding/pem" - - "golang.org/x/oauth2" - "gopkg.in/square/go-jose.v2" - - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" -) - -//TokenExchangeRP extends the `RelayingParty` interface for the *draft* oauth2 `Token Exchange` -type TokenExchangeRP interface { - RelayingParty - - //TokenExchange implement the `Token Exchange Grant` exchanging some token for an other - TokenExchange(context.Context, *tokenexchange.TokenExchangeRequest) (*oauth2.Token, error) -} - -//DelegationTokenExchangeRP extends the `TokenExchangeRP` interface -//for the specific `delegation token` request -type DelegationTokenExchangeRP interface { - TokenExchangeRP - - //DelegationTokenExchange implement the `Token Exchange Grant` - //providing an access token in request for a `delegation` token for a given resource / audience - DelegationTokenExchange(context.Context, string, ...tokenexchange.TokenExchangeOption) (*oauth2.Token, error) -} - -//TokenExchange handles the oauth2 token exchange -func TokenExchange(ctx context.Context, request *tokenexchange.TokenExchangeRequest, rp RelayingParty) (newToken *oauth2.Token, err error) { - return CallTokenEndpoint(request, rp) -} - -//DelegationTokenExchange handles the oauth2 token exchange for a delegation token -func DelegationTokenExchange(ctx context.Context, subjectToken string, rp RelayingParty, reqOpts ...tokenexchange.TokenExchangeOption) (newToken *oauth2.Token, err error) { - return TokenExchange(ctx, DelegationTokenRequest(subjectToken, reqOpts...), rp) -} - -//JWTProfileExchange handles the oauth2 jwt profile exchange -func JWTProfileExchange(ctx context.Context, jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, rp RelayingParty) (*oauth2.Token, error) { - return CallTokenEndpoint(jwtProfileGrantRequest, rp) -} - -//JWTProfileExchange handles the oauth2 jwt profile exchange -func JWTProfileAssertionExchange(ctx context.Context, assertion *oidc.JWTProfileAssertion, scopes oidc.Scopes, rp RelayingParty) (*oauth2.Token, error) { - token, err := GenerateJWTProfileToken(assertion) - if err != nil { - return nil, err - } - return JWTProfileExchange(ctx, oidc.NewJWTProfileGrantRequest(token, scopes...), rp) -} - -func GenerateJWTProfileToken(assertion *oidc.JWTProfileAssertion) (string, error) { - privateKey, err := bytesToPrivateKey(assertion.PrivateKey) - if err != nil { - return "", err - } - key := jose.SigningKey{ - Algorithm: jose.RS256, - Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID}, - } - signer, err := jose.NewSigner(key, &jose.SignerOptions{}) - if err != nil { - return "", err - } - - marshalledAssertion, err := json.Marshal(assertion) - if err != nil { - return "", err - } - signedAssertion, err := signer.Sign(marshalledAssertion) - if err != nil { - return "", err - } - return signedAssertion.CompactSerialize() -} - -func bytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) { - block, _ := pem.Decode(priv) - enc := x509.IsEncryptedPEMBlock(block) - b := block.Bytes - var err error - if enc { - b, err = x509.DecryptPEMBlock(block, nil) - if err != nil { - return nil, err - } - } - key, err := x509.ParsePKCS1PrivateKey(b) - if err != nil { - return nil, err - } - return key, nil -} diff --git a/pkg/utils/key.go b/pkg/utils/key.go new file mode 100644 index 0000000..7965c85 --- /dev/null +++ b/pkg/utils/key.go @@ -0,0 +1,25 @@ +package utils + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" +) + +func BytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(priv) + enc := x509.IsEncryptedPEMBlock(block) + b := block.Bytes + var err error + if enc { + b, err = x509.DecryptPEMBlock(block, nil) + if err != nil { + return nil, err + } + } + key, err := x509.ParsePKCS1PrivateKey(b) + if err != nil { + return nil, err + } + return key, nil +} From 01ff740f4e6960053b8eec60c37fe876d3c77e77 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 12 Feb 2021 06:47:16 +0100 Subject: [PATCH 024/502] fixes --- pkg/oidc/discovery.go | 2 +- pkg/op/discovery_test.go | 31 +++++++++++++------ pkg/op/mock/storage.mock.go | 59 ++++++++++++++++++------------------- 3 files changed, 52 insertions(+), 40 deletions(-) diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go index ef1d65e..acab578 100644 --- a/pkg/oidc/discovery.go +++ b/pkg/oidc/discovery.go @@ -19,7 +19,7 @@ type DiscoveryConfiguration struct { CheckSessionIframe string `json:"check_session_iframe,omitempty"` JwksURI string `json:"jwks_uri,omitempty"` ScopesSupported []string `json:"scopes_supported,omitempty"` - ResponseTypesSupported []string `json:"response_types_supported"` + ResponseTypesSupported []string `json:"response_types_supported,omitempty"` ResponseModesSupported []string `json:"response_modes_supported,omitempty"` GrantTypesSupported []GrantType `json:"grant_types_supported,omitempty"` ACRValuesSupported []string `json:"acr_values_supported,omitempty"` diff --git a/pkg/op/discovery_test.go b/pkg/op/discovery_test.go index e479faa..4d97a01 100644 --- a/pkg/op/discovery_test.go +++ b/pkg/op/discovery_test.go @@ -37,7 +37,7 @@ func TestDiscover(t *testing.T) { op.Discover(tt.args.w, tt.args.config) rec := tt.args.w.(*httptest.ResponseRecorder) require.Equal(t, http.StatusOK, rec.Code) - require.Equal(t, `{"issuer":"https://issuer.com"}`, rec.Body.String()) + require.Equal(t, `{"issuer":"https://issuer.com","request_uri_parameter_supported":false}`, rec.Body.String()) }) } } @@ -199,36 +199,49 @@ func Test_SubjectTypes(t *testing.T) { } } -func Test_AuthMethods(t *testing.T) { - m := mock.NewMockConfiguration(gomock.NewController(t)) +func Test_AuthMethodsTokenEndpoint(t *testing.T) { type args struct { c op.Configuration } tests := []struct { name string args args - want []string + want []oidc.AuthMethod }{ { - "imlicit basic", + "none and basic", args{func() op.Configuration { + m := mock.NewMockConfiguration(gomock.NewController(t)) m.EXPECT().AuthMethodPostSupported().Return(false) + m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false) return m }()}, - []string{string(oidc.AuthMethodBasic)}, + []oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic}, }, { - "basic and post", + "none, basic and post", args{func() op.Configuration { + m := mock.NewMockConfiguration(gomock.NewController(t)) m.EXPECT().AuthMethodPostSupported().Return(true) + m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false) return m }()}, - []string{string(oidc.AuthMethodBasic), string(oidc.AuthMethodPost)}, + []oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost}, + }, + { + "none, basic, post and private_key_jwt", + args{func() op.Configuration { + m := mock.NewMockConfiguration(gomock.NewController(t)) + m.EXPECT().AuthMethodPostSupported().Return(true) + m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true) + return m + }()}, + []oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost, oidc.AuthMethodPrivateKeyJWT}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := op.AuthMethods(tt.args.c); !reflect.DeepEqual(got, tt.want) { + if got := op.AuthMethodsTokenEndpoint(tt.args.c); !reflect.DeepEqual(got, tt.want) { t.Errorf("authMethods() = %v, want %v", got, tt.want) } }) diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index b9adcec..e589413 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -198,36 +198,6 @@ func (mr *MockStorageMockRecorder) GetSigningKey(arg0, arg1, arg2, arg3 interfac return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSigningKey", reflect.TypeOf((*MockStorage)(nil).GetSigningKey), arg0, arg1, arg2, arg3) } -// GetUserinfoFromScopes mocks base method -func (m *MockStorage) GetUserinfoFromScopes(arg0 context.Context, arg1, arg2 string, arg3 []string) (oidc.UserInfo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserinfoFromScopes", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(oidc.UserInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserinfoFromScopes indicates an expected call of GetUserinfoFromScopes -func (mr *MockStorageMockRecorder) GetUserinfoFromScopes(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserinfoFromScopes", reflect.TypeOf((*MockStorage)(nil).GetUserinfoFromScopes), arg0, arg1, arg2, arg3) -} - -// GetUserinfoFromToken mocks base method -func (m *MockStorage) GetUserinfoFromToken(arg0 context.Context, arg1, arg2, arg3 string) (oidc.UserInfo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserinfoFromToken", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(oidc.UserInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserinfoFromToken indicates an expected call of GetUserinfoFromToken -func (mr *MockStorageMockRecorder) GetUserinfoFromToken(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserinfoFromToken", reflect.TypeOf((*MockStorage)(nil).GetUserinfoFromToken), arg0, arg1, arg2, arg3) -} - // Health mocks base method func (m *MockStorage) Health(arg0 context.Context) error { m.ctrl.T.Helper() @@ -270,6 +240,20 @@ func (mr *MockStorageMockRecorder) SaveNewKeyPair(arg0 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveNewKeyPair", reflect.TypeOf((*MockStorage)(nil).SaveNewKeyPair), arg0) } +// SetIntrospectionFromToken mocks base method +func (m *MockStorage) SetIntrospectionFromToken(arg0 context.Context, arg1 oidc.IntrospectionResponse, arg2, arg3, arg4 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetIntrospectionFromToken", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetIntrospectionFromToken indicates an expected call of SetIntrospectionFromToken +func (mr *MockStorageMockRecorder) SetIntrospectionFromToken(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIntrospectionFromToken", reflect.TypeOf((*MockStorage)(nil).SetIntrospectionFromToken), arg0, arg1, arg2, arg3, arg4) +} + // SetUserinfoFromScopes mocks base method func (m *MockStorage) SetUserinfoFromScopes(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3 string, arg4 []string) error { m.ctrl.T.Helper() @@ -311,3 +295,18 @@ func (mr *MockStorageMockRecorder) TerminateSession(arg0, arg1, arg2 interface{} mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TerminateSession", reflect.TypeOf((*MockStorage)(nil).TerminateSession), arg0, arg1, arg2) } + +// ValidateJWTProfileScopes mocks base method +func (m *MockStorage) ValidateJWTProfileScopes(arg0 context.Context, arg1 string, arg2 oidc.Scopes) (oidc.Scopes, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateJWTProfileScopes", arg0, arg1, arg2) + ret0, _ := ret[0].(oidc.Scopes) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ValidateJWTProfileScopes indicates an expected call of ValidateJWTProfileScopes +func (mr *MockStorageMockRecorder) ValidateJWTProfileScopes(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateJWTProfileScopes", reflect.TypeOf((*MockStorage)(nil).ValidateJWTProfileScopes), arg0, arg1, arg2) +} From 0c7b2605bdc5ccdb96a2305f15c396fb3f4a3a8a Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 12 Feb 2021 07:02:10 +0100 Subject: [PATCH 025/502] clenaup --- example/client/service/service.go | 34 ++++++++----------------------- example/internal/mock/storage.go | 21 ++++++++++++++----- pkg/client/profile/jwt_profile.go | 10 ++++++++- pkg/oidc/introspection.go | 10 +++------ pkg/oidc/types.go | 9 ++++---- pkg/oidc/userinfo.go | 5 ----- pkg/op/discovery.go | 24 +++++++++------------- pkg/utils/http.go | 3 --- 8 files changed, 51 insertions(+), 65 deletions(-) diff --git a/example/client/service/service.go b/example/client/service/service.go index 95227d0..34d959d 100644 --- a/example/client/service/service.go +++ b/example/client/service/service.go @@ -21,24 +21,18 @@ var ( ) func main() { - //keyPath := os.Getenv("KEY_PATH") + keyPath := os.Getenv("KEY_PATH") issuer := os.Getenv("ISSUER") port := os.Getenv("PORT") scopes := strings.Split(os.Getenv("SCOPES"), " ") - //testURL := os.Getenv("TEST_URL") - //if keyPath != "" { - // ts, err := rp.NewJWTProfileTokenSourceFromFile(issuer, keyPath, scopes) - // if err != nil { - // logrus.Fatalf("error creating token source %s", err.Error()) - // } - // //client = oauth2.NewClient(context.Background(), ts) - // resp, err := callExampleEndpoint(client, testURL) - // if err != nil { - // logrus.Fatalf("error response from test url: %s", err.Error()) - // } - // fmt.Println(resp) - //} + if keyPath != "" { + ts, err := profile.NewJWTProfileTokenSourceFromKeyFile(issuer, keyPath, scopes) + if err != nil { + logrus.Fatalf("error creating token source %s", err.Error()) + } + client = oauth2.NewClient(context.Background(), ts) + } http.HandleFunc("/jwt-profile", func(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { @@ -84,7 +78,7 @@ func main() { http.Error(w, err.Error(), http.StatusInternalServerError) return } - ts, err := profile.NewJWTProfileTokenSourceFromKeyFile(issuer, key, scopes) + ts, err := profile.NewJWTProfileTokenSourceFromKeyFileData(issuer, key, scopes) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -95,16 +89,6 @@ func main() { http.Error(w, err.Error(), http.StatusInternalServerError) return } - //assertion, err := oidc.NewJWTProfileAssertionFromFileData(key, []string{issuer}) - //if err != nil { - // http.Error(w, err.Error(), http.StatusInternalServerError) - // return - //} - //token, err := rp.JWTProfileAssertionExchange(ctx, assertion, scopes, provider) - //if err != nil { - // http.Error(w, err.Error(), http.StatusInternalServerError) - // return - //} data, err := json.Marshal(token) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index 40a1f86..e04c045 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -210,23 +210,34 @@ func (s *AuthStorage) AuthorizeClientIDSecret(_ context.Context, id string, _ st return nil } -func (s *AuthStorage) GetUserinfoFromToken(ctx context.Context, _, _, _ string) (oidc.UserInfo, error) { - return s.GetUserinfoFromScopes(ctx, "", "", []string{}) +func (s *AuthStorage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, _, _, _ string) error { + return s.SetUserinfoFromScopes(ctx, userinfo, "", "", []string{}) } -func (s *AuthStorage) GetUserinfoFromScopes(_ context.Context, _, _ string, _ []string) (oidc.UserInfo, error) { - userinfo := oidc.NewUserInfo() +func (s *AuthStorage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, _, _ string, _ []string) error { userinfo.SetSubject(a.GetSubject()) userinfo.SetAddress(oidc.NewUserInfoAddress("Test 789\nPostfach 2", "", "", "", "", "")) userinfo.SetEmail("test", true) userinfo.SetPhone("0791234567", true) userinfo.SetName("Test") userinfo.AppendClaims("private_claim", "test") - return userinfo, nil + return nil } func (s *AuthStorage) GetPrivateClaimsFromScopes(_ context.Context, _, _ string, _ []string) (map[string]interface{}, error) { return map[string]interface{}{"private_claim": "test"}, nil } +func (s *AuthStorage) SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, clientID string) error { + if err := s.SetUserinfoFromScopes(ctx, userinfo, "", "", []string{}); err != nil { + return err + } + userinfo.SetClientID(a.ClientID) + return nil +} + +func (s *AuthStorage) ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) { + return scope, nil +} + type ConfClient struct { applicationType op.ApplicationType authMethod oidc.AuthMethod diff --git a/pkg/client/profile/jwt_profile.go b/pkg/client/profile/jwt_profile.go index d60a2f8..46a0fe9 100644 --- a/pkg/client/profile/jwt_profile.go +++ b/pkg/client/profile/jwt_profile.go @@ -23,7 +23,15 @@ type jwtProfileTokenSource struct { tokenEndpoint string } -func NewJWTProfileTokenSourceFromKeyFile(issuer string, data []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) { +func NewJWTProfileTokenSourceFromKeyFile(issuer, keyPath string, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) { + keyData, err := client.ConfigFromKeyFile(keyPath) + if err != nil { + return nil, err + } + return NewJWTProfileTokenSource(issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...) +} + +func NewJWTProfileTokenSourceFromKeyFileData(issuer string, data []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) { keyData, err := client.ConfigFromKeyFileData(data) if err != nil { return nil, err diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index 1a66520..a2176aa 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -21,7 +21,7 @@ type IntrospectionResponse interface { UserInfoSetter SetActive(bool) IsActive() bool - SetScopes(scopes Scope) + SetScopes(scopes Scopes) SetClientID(id string) } @@ -31,7 +31,7 @@ func NewIntrospectionResponse() IntrospectionResponse { type introspectionResponse struct { Active bool `json:"active"` - Scope Scope `json:"scope,omitempty"` + Scope Scopes `json:"scope,omitempty"` ClientID string `json:"client_id,omitempty"` Subject string `json:"sub,omitempty"` userInfoProfile @@ -46,7 +46,7 @@ func (u *introspectionResponse) IsActive() bool { return u.Active } -func (u *introspectionResponse) SetScopes(scope Scope) { +func (u *introspectionResponse) SetScopes(scope Scopes) { u.Scope = scope } @@ -252,10 +252,6 @@ func (i *introspectionResponse) MarshalJSON() ([]byte, error) { } return json.Marshal(i.claims) - //if err != nil { - // return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims) - //} - //return utils.ConcatenateJSON(b, claims) } func (i *introspectionResponse) UnmarshalJSON(data []byte) error { diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index fd496da..5525923 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -59,7 +59,6 @@ type Prompt string type ResponseType string type Scopes []string -type Scope []string //TODO: hurst? func (s Scopes) Encode() string { return strings.Join(s, " ") @@ -74,16 +73,16 @@ func (s *Scopes) MarshalText() ([]byte, error) { return []byte(s.Encode()), nil } -func (s *Scope) MarshalJSON() ([]byte, error) { - return json.Marshal(Scopes(*s).Encode()) +func (s *Scopes) MarshalJSON() ([]byte, error) { + return json.Marshal((*s).Encode()) } -func (s *Scope) UnmarshalJSON(data []byte) error { +func (s *Scopes) UnmarshalJSON(data []byte) error { var str string if err := json.Unmarshal(data, &str); err != nil { return err } - *s = Scope(strings.Split(str, " ")) + *s = strings.Split(str, " ") return nil } diff --git a/pkg/oidc/userinfo.go b/pkg/oidc/userinfo.go index 3a92501..6bc0016 100644 --- a/pkg/oidc/userinfo.go +++ b/pkg/oidc/userinfo.go @@ -355,11 +355,6 @@ func (i *userinfo) MarshalJSON() ([]byte, error) { } return json.Marshal(i.claims) - //claims, err := json.Marshal(i.claims) - //if err != nil { - // return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims) - //} - //return utils.ConcatenateJSON(b, claims) } func (i *userinfo) UnmarshalJSON(data []byte) error { diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 291214c..d8ef7c3 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -21,20 +21,16 @@ func Discover(w http.ResponseWriter, config *oidc.DiscoveryConfiguration) { func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfiguration { return &oidc.DiscoveryConfiguration{ - Issuer: c.Issuer(), - AuthorizationEndpoint: c.AuthorizationEndpoint().Absolute(c.Issuer()), - TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()), - IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()), - UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()), - //RevocationEndpoint: c.RevocationEndpoint().Absolute(c.Issuer()), - EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), - // CheckSessionIframe: c.TokenEndpoint().Absolute(c.Issuer())(c.CheckSessionIframe), - JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), - ScopesSupported: Scopes(c), - ResponseTypesSupported: ResponseTypes(c), - //ResponseModesSupported: - GrantTypesSupported: GrantTypes(c), - //ACRValuesSupported: ACRValues(c), + Issuer: c.Issuer(), + AuthorizationEndpoint: c.AuthorizationEndpoint().Absolute(c.Issuer()), + TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()), + IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()), + UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()), + EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), + JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), + ScopesSupported: Scopes(c), + ResponseTypesSupported: ResponseTypes(c), + GrantTypesSupported: GrantTypes(c), SubjectTypesSupported: SubjectTypes(c), IDTokenSigningAlgValuesSupported: SigAlgorithms(s), TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(c), diff --git a/pkg/utils/http.go b/pkg/utils/http.go index 6f1b74d..fa51815 100644 --- a/pkg/utils/http.go +++ b/pkg/utils/http.go @@ -42,9 +42,6 @@ func FormRequest(endpoint string, request interface{}, encoder Encoder, authFn i if fn, ok := authFn.(FormAuthorization); ok { fn(form) } - if fn, ok := authFn.(func(url.Values)); ok { - fn(form) - } body := strings.NewReader(form.Encode()) req, err := http.NewRequest("POST", endpoint, body) if err != nil { From fb9d1b3c4a47e8e0341413d24c23eb5daf7c852f Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 12 Feb 2021 12:51:22 +0100 Subject: [PATCH 026/502] Update example/internal/mock/storage.go Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com> --- example/internal/mock/storage.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index e04c045..8ce59de 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -226,11 +226,11 @@ func (s *AuthStorage) GetPrivateClaimsFromScopes(_ context.Context, _, _ string, return map[string]interface{}{"private_claim": "test"}, nil } -func (s *AuthStorage) SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, clientID string) error { - if err := s.SetUserinfoFromScopes(ctx, userinfo, "", "", []string{}); err != nil { +func (s *AuthStorage) SetIntrospectionFromToken(ctx context.Context, introspect oidc.IntrospectionResponse, tokenID, subject, clientID string) error { + if err := s.SetUserinfoFromScopes(ctx, introspect, "", "", []string{}); err != nil { return err } - userinfo.SetClientID(a.ClientID) + introspect.SetClientID(a.ClientID) return nil } From 5678693d44b8b1628a7be2e3dd2d02630c4325f1 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 12 Feb 2021 12:52:12 +0100 Subject: [PATCH 027/502] clenaup --- pkg/op/op.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pkg/op/op.go b/pkg/op/op.go index ee41630..26445c5 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -25,13 +25,6 @@ const ( defaultUserinfoEndpoint = "userinfo" defaultEndSessionEndpoint = "end_session" defaultKeysEndpoint = "keys" - - //AuthMethodBasic AuthMethod = "client_secret_basic" - //AuthMethodPost AuthMethod = "client_secret_post" - //AuthMethodNone AuthMethod = "none" - //AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt" - - //CodeMethodS256 = "S256" ) var ( From 1518c843de7730cb272d609a20ac3d4e787f1581 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 15 Feb 2021 13:43:50 +0100 Subject: [PATCH 028/502] feat: token introspection (#83) * introspect * introspect and client assertion * introspect and client assertion * scopes * token introspection * introspect * refactoring * fixes * clenaup * Update example/internal/mock/storage.go Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com> * clenaup Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com> --- example/client/api/api.go | 167 ++++++----- example/client/app/app.go | 99 +------ example/client/github/github.go | 10 +- example/client/service/service.go | 180 ++++++++++++ example/internal/mock/storage.go | 33 ++- pkg/client/client.go | 90 ++++++ pkg/client/jwt_profile.go | 30 ++ pkg/client/key.go | 40 +++ pkg/client/profile/jwt_profile.go | 93 ++++++ pkg/{ => client}/rp/cli/cli.go | 8 +- pkg/{ => client}/rp/delegation.go | 0 pkg/{ => client}/rp/jwks.go | 0 pkg/{ => client}/rp/mock/generate.go | 0 pkg/{ => client}/rp/mock/verifier.mock.go | 10 +- pkg/{ => client}/rp/relaying_party.go | 201 +++++++------ pkg/client/rp/tockenexchange.go | 27 ++ pkg/{ => client}/rp/verifier.go | 10 - pkg/client/rs/resource_server.go | 123 ++++++++ pkg/oidc/code_challenge.go | 2 +- pkg/oidc/discovery.go | 77 +++-- .../grants/tokenexchange/tokenexchange.go | 20 -- pkg/oidc/introspection.go | 276 ++++++++++++++++++ pkg/oidc/jwt_profile.go | 18 ++ pkg/oidc/token.go | 63 +++- pkg/oidc/token_request.go | 16 +- pkg/oidc/types.go | 13 + pkg/oidc/userinfo.go | 11 +- pkg/op/authrequest.go | 18 ++ pkg/op/client.go | 2 +- pkg/op/config.go | 2 + pkg/op/discovery.go | 81 +++-- pkg/op/discovery_test.go | 31 +- pkg/op/mock/client.mock.go | 4 +- pkg/op/mock/configuration.mock.go | 28 ++ pkg/op/mock/storage.mock.go | 87 ++++-- pkg/op/mock/storage.mock.impl.go | 12 +- pkg/op/op.go | 36 ++- pkg/op/probes.go | 2 +- pkg/op/storage.go | 23 +- pkg/op/token.go | 3 +- pkg/op/token_intospection.go | 77 +++++ pkg/op/tokenrequest.go | 53 +++- pkg/op/userinfo.go | 31 +- pkg/op/verifier_jwt_profile.go | 10 +- pkg/rp/tockenexchange.go | 100 ------- pkg/utils/key.go | 25 ++ 46 files changed, 1672 insertions(+), 570 deletions(-) create mode 100644 example/client/service/service.go create mode 100644 pkg/client/client.go create mode 100644 pkg/client/jwt_profile.go create mode 100644 pkg/client/key.go create mode 100644 pkg/client/profile/jwt_profile.go rename pkg/{ => client}/rp/cli/cli.go (67%) rename pkg/{ => client}/rp/delegation.go (100%) rename pkg/{ => client}/rp/jwks.go (100%) rename pkg/{ => client}/rp/mock/generate.go (100%) rename pkg/{ => client}/rp/mock/verifier.mock.go (94%) rename pkg/{ => client}/rp/relaying_party.go (72%) create mode 100644 pkg/client/rp/tockenexchange.go rename pkg/{ => client}/rp/verifier.go (92%) create mode 100644 pkg/client/rs/resource_server.go create mode 100644 pkg/oidc/introspection.go create mode 100644 pkg/oidc/jwt_profile.go create mode 100644 pkg/op/token_intospection.go delete mode 100644 pkg/rp/tockenexchange.go create mode 100644 pkg/utils/key.go diff --git a/example/client/api/api.go b/example/client/api/api.go index 6e1b0bd..a3ae85e 100644 --- a/example/client/api/api.go +++ b/example/client/api/api.go @@ -1,90 +1,103 @@ package main -// import ( -// "encoding/json" -// "fmt" -// "log" -// "net/http" -// "os" +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "strings" + "time" -// "github.com/caos/oidc/pkg/oidc" -// "github.com/caos/oidc/pkg/oidc/rp" -// "github.com/caos/utils/logging" -// ) + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" -// const ( -// publicURL string = "/public" -// protectedURL string = "/protected" -// protectedExchangeURL string = "/protected/exchange" -// ) + "github.com/caos/oidc/pkg/client/rs" + "github.com/caos/oidc/pkg/oidc" +) + +const ( + publicURL string = "/public" + protectedURL string = "/protected" + protectedClaimURL string = "/protected/{claim}/{value}" +) func main() { - // clientID := os.Getenv("CLIENT_ID") - // clientSecret := os.Getenv("CLIENT_SECRET") - // issuer := os.Getenv("ISSUER") - // port := os.Getenv("PORT") + keyPath := os.Getenv("KEY") + port := os.Getenv("PORT") + issuer := os.Getenv("ISSUER") - // // ctx := context.Background() + provider, err := rs.NewResourceServerFromKeyFile(issuer, keyPath) + if err != nil { + logrus.Fatalf("error creating provider %s", err.Error()) + } - // providerConfig := &oidc.ProviderConfig{ - // ClientID: clientID, - // ClientSecret: clientSecret, - // Issuer: issuer, - // } - // provider, err := rp.NewDefaultProvider(providerConfig) - // logging.Log("APP-nx6PeF").OnError(err).Panic("error creating provider") + router := mux.NewRouter() - // http.HandleFunc(publicURL, func(w http.ResponseWriter, r *http.Request) { - // w.Write([]byte("OK")) - // }) + //public url accessible without any authorization + //will print `OK` and current timestamp + router.HandleFunc(publicURL, func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("OK " + time.Now().String())) + }) - // http.HandleFunc(protectedURL, func(w http.ResponseWriter, r *http.Request) { - // ok, token := checkToken(w, r) - // if !ok { - // return - // } - // resp, err := provider.Introspect(r.Context(), token) - // if err != nil { - // http.Error(w, err.Error(), http.StatusForbidden) - // return - // } - // data, err := json.Marshal(resp) - // if err != nil { - // http.Error(w, err.Error(), http.StatusInternalServerError) - // return - // } - // w.Write(data) - // }) + //protected url which needs an active token + //will print the result of the introspection endpoint on success + router.HandleFunc(protectedURL, func(w http.ResponseWriter, r *http.Request) { + ok, token := checkToken(w, r) + if !ok { + return + } + resp, err := rs.Introspect(r.Context(), provider, token) + if err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + data, err := json.Marshal(resp) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write(data) + }) - // http.HandleFunc(protectedExchangeURL, func(w http.ResponseWriter, r *http.Request) { - // ok, token := checkToken(w, r) - // if !ok { - // return - // } - // tokens, err := provider.DelegationTokenExchange(r.Context(), token, oidc.WithResource([]string{"Test"})) - // if err != nil { - // http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) - // return - // } + //protected url which needs an active token and checks if the response of the introspect endpoint + //contains a requested claim with the required (string) value + //e.g. /protected/username/livio@caos.ch + router.HandleFunc(protectedClaimURL, func(w http.ResponseWriter, r *http.Request) { + ok, token := checkToken(w, r) + if !ok { + return + } + resp, err := rs.Introspect(r.Context(), provider, token) + if err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + params := mux.Vars(r) + requestedClaim := params["claim"] + requestedValue := params["value"] + value, ok := resp.GetClaim(requestedClaim).(string) + if !ok || value == "" || value != requestedValue { + http.Error(w, "claim does not match", http.StatusForbidden) + return + } + w.Write([]byte("authorized with value " + value)) + }) - // data, err := json.Marshal(tokens) - // if err != nil { - // http.Error(w, err.Error(), http.StatusInternalServerError) - // return - // } - // w.Write(data) - // }) - - // lis := fmt.Sprintf("127.0.0.1:%s", port) - // log.Printf("listening on http://%s/", lis) - // log.Fatal(http.ListenAndServe(lis, nil)) - // } - - // func checkToken(w http.ResponseWriter, r *http.Request) (bool, string) { - // token := r.Header.Get("authorization") - // if token == "" { - // http.Error(w, "Auth header missing", http.StatusUnauthorized) - // return false, "" - // } - // return true, token + lis := fmt.Sprintf("127.0.0.1:%s", port) + log.Printf("listening on http://%s/", lis) + log.Fatal(http.ListenAndServe(lis, router)) +} + +func checkToken(w http.ResponseWriter, r *http.Request) (bool, string) { + auth := r.Header.Get("authorization") + if auth == "" { + http.Error(w, "auth header missing", http.StatusUnauthorized) + return false, "" + } + if !strings.HasPrefix(auth, oidc.PrefixBearer) { + http.Error(w, "invalid header", http.StatusUnauthorized) + return false, "" + } + return true, strings.TrimPrefix(auth, oidc.PrefixBearer) } diff --git a/example/client/app/app.go b/example/client/app/app.go index ad00adb..e3ddd15 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -1,11 +1,8 @@ package main import ( - "context" "encoding/json" "fmt" - "html/template" - "io/ioutil" "net/http" "os" "strings" @@ -14,8 +11,8 @@ import ( "github.com/google/uuid" "github.com/sirupsen/logrus" + "github.com/caos/oidc/pkg/client/rp" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/rp" "github.com/caos/oidc/pkg/utils" ) @@ -27,18 +24,26 @@ var ( func main() { clientID := os.Getenv("CLIENT_ID") clientSecret := os.Getenv("CLIENT_SECRET") + keyPath := os.Getenv("KEY_PATH") issuer := os.Getenv("ISSUER") port := os.Getenv("PORT") scopes := strings.Split(os.Getenv("SCOPES"), " ") - ctx := context.Background() - redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath) cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) - provider, err := rp.NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, - rp.WithPKCE(cookieHandler), - rp.WithVerifierOpts(rp.WithIssuedAtOffset(5*time.Second)), - ) + + options := []rp.Option{ + rp.WithCookieHandler(cookieHandler), + rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)), + } + if clientSecret == "" { + options = append(options, rp.WithPKCE(cookieHandler)) + } + if keyPath != "" { + options = append(options, rp.WithClientKey(keyPath)) + } + + provider, err := rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...) if err != nil { logrus.Fatalf("error creating provider %s", err.Error()) } @@ -71,80 +76,6 @@ func main() { //with the returned tokens from the token endpoint http.Handle(callbackPath, rp.CodeExchangeHandler(marshal, provider)) - http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { - tokens, err := rp.ClientCredentials(ctx, provider, "scope") - if err != nil { - http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) - return - } - - data, err := json.Marshal(tokens) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Write(data) - }) - - http.HandleFunc("/jwt-profile", func(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - tpl := ` - - - - - Login - - -
- - - -
- - ` - t, err := template.New("login").Parse(tpl) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - err = t.Execute(w, nil) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - } else { - err := r.ParseMultipartForm(4 << 10) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - file, handler, err := r.FormFile("key") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer file.Close() - - key, err := ioutil.ReadAll(file) - fmt.Println(handler.Header) - assertion, err := oidc.NewJWTProfileAssertionFromFileData(key, []string{issuer}) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - token, err := rp.JWTProfileAssertionExchange(ctx, assertion, scopes, provider) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - data, err := json.Marshal(token) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Write(data) - } - }) lis := fmt.Sprintf("127.0.0.1:%s", port) logrus.Infof("listening on http://%s/", lis) logrus.Fatal(http.ListenAndServe("127.0.0.1:"+port, nil)) diff --git a/example/client/github/github.go b/example/client/github/github.go index c136091..f39c40b 100644 --- a/example/client/github/github.go +++ b/example/client/github/github.go @@ -10,8 +10,8 @@ import ( "golang.org/x/oauth2" githubOAuth "golang.org/x/oauth2/github" - "github.com/caos/oidc/pkg/rp" - "github.com/caos/oidc/pkg/rp/cli" + "github.com/caos/oidc/pkg/client/rp" + "github.com/caos/oidc/pkg/client/rp/cli" "github.com/caos/oidc/pkg/utils" ) @@ -35,7 +35,7 @@ func main() { ctx := context.Background() cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) - relayingParty, err := rp.NewRelayingPartyOAuth(rpConfig, rp.WithCookieHandler(cookieHandler)) + relyingParty, err := rp.NewRelyingPartyOAuth(rpConfig, rp.WithCookieHandler(cookieHandler)) if err != nil { fmt.Printf("error creating relaying party: %v", err) return @@ -43,9 +43,9 @@ func main() { state := func() string { return uuid.New().String() } - token := cli.CodeFlow(relayingParty, callbackPath, port, state) + token := cli.CodeFlow(relyingParty, callbackPath, port, state) - client := github.NewClient(relayingParty.OAuthConfig().Client(ctx, token.Token)) + client := github.NewClient(relyingParty.OAuthConfig().Client(ctx, token.Token)) _, _, err = client.Users.Get(ctx, "") if err != nil { diff --git a/example/client/service/service.go b/example/client/service/service.go new file mode 100644 index 0000000..34d959d --- /dev/null +++ b/example/client/service/service.go @@ -0,0 +1,180 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "html/template" + "io/ioutil" + "net/http" + "os" + "strings" + + "github.com/sirupsen/logrus" + "golang.org/x/oauth2" + + "github.com/caos/oidc/pkg/client/profile" +) + +var ( + client *http.Client = http.DefaultClient +) + +func main() { + keyPath := os.Getenv("KEY_PATH") + issuer := os.Getenv("ISSUER") + port := os.Getenv("PORT") + scopes := strings.Split(os.Getenv("SCOPES"), " ") + + if keyPath != "" { + ts, err := profile.NewJWTProfileTokenSourceFromKeyFile(issuer, keyPath, scopes) + if err != nil { + logrus.Fatalf("error creating token source %s", err.Error()) + } + client = oauth2.NewClient(context.Background(), ts) + } + + http.HandleFunc("/jwt-profile", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + tpl := ` + + + + + Login + + +
+ + + +
+ + ` + t, err := template.New("login").Parse(tpl) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + err = t.Execute(w, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } else { + err := r.ParseMultipartForm(4 << 10) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + file, _, err := r.FormFile("key") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer file.Close() + + key, err := ioutil.ReadAll(file) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + ts, err := profile.NewJWTProfileTokenSourceFromKeyFileData(issuer, key, scopes) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + client = oauth2.NewClient(context.Background(), ts) + token, err := ts.Token() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + data, err := json.Marshal(token) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write(data) + } + }) + + http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { + tpl := ` + + + + + Test + + +
+ + + +
+ {{if .URL}} +

+ Result for {{.URL}}: {{.Response}} +

+ {{end}} + + ` + err := r.ParseForm() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + testURL := r.Form.Get("url") + var data struct { + URL string + Response interface{} + } + if testURL != "" { + data.URL = testURL + data.Response, err = callExampleEndpoint(client, testURL) + if err != nil { + data.Response = err + } + } + t, err := template.New("login").Parse(tpl) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + err = t.Execute(w, data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + }) + lis := fmt.Sprintf("127.0.0.1:%s", port) + logrus.Infof("listening on http://%s/", lis) + logrus.Fatal(http.ListenAndServe("127.0.0.1:"+port, nil)) +} + +func callExampleEndpoint(client *http.Client, testURL string) (interface{}, error) { + req, err := http.NewRequest("GET", testURL, nil) + if err != nil { + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("http status not ok: %s %s", resp.Status, body) + } + + if strings.HasPrefix(resp.Header.Get("content-type"), "text/plain") { + return string(body), nil + } + return body, err +} diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index 8c1ab38..8ce59de 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -184,22 +184,22 @@ func (s *AuthStorage) GetClientByClientID(_ context.Context, id string) (op.Clie return nil, errors.New("not found") } var appType op.ApplicationType - var authMethod op.AuthMethod + var authMethod oidc.AuthMethod var accessTokenType op.AccessTokenType var responseTypes []oidc.ResponseType if id == "web" { appType = op.ApplicationTypeWeb - authMethod = op.AuthMethodBasic + authMethod = oidc.AuthMethodBasic accessTokenType = op.AccessTokenTypeBearer responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode} } else if id == "native" { appType = op.ApplicationTypeNative - authMethod = op.AuthMethodNone + authMethod = oidc.AuthMethodNone accessTokenType = op.AccessTokenTypeBearer responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode} } else { appType = op.ApplicationTypeUserAgent - authMethod = op.AuthMethodNone + authMethod = oidc.AuthMethodNone accessTokenType = op.AccessTokenTypeJWT responseTypes = []oidc.ResponseType{oidc.ResponseTypeIDToken, oidc.ResponseTypeIDTokenOnly} } @@ -210,26 +210,37 @@ func (s *AuthStorage) AuthorizeClientIDSecret(_ context.Context, id string, _ st return nil } -func (s *AuthStorage) GetUserinfoFromToken(ctx context.Context, _, _, _ string) (oidc.UserInfo, error) { - return s.GetUserinfoFromScopes(ctx, "", "", []string{}) +func (s *AuthStorage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, _, _, _ string) error { + return s.SetUserinfoFromScopes(ctx, userinfo, "", "", []string{}) } -func (s *AuthStorage) GetUserinfoFromScopes(_ context.Context, _, _ string, _ []string) (oidc.UserInfo, error) { - userinfo := oidc.NewUserInfo() +func (s *AuthStorage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, _, _ string, _ []string) error { userinfo.SetSubject(a.GetSubject()) userinfo.SetAddress(oidc.NewUserInfoAddress("Test 789\nPostfach 2", "", "", "", "", "")) userinfo.SetEmail("test", true) userinfo.SetPhone("0791234567", true) userinfo.SetName("Test") userinfo.AppendClaims("private_claim", "test") - return userinfo, nil + return nil } func (s *AuthStorage) GetPrivateClaimsFromScopes(_ context.Context, _, _ string, _ []string) (map[string]interface{}, error) { return map[string]interface{}{"private_claim": "test"}, nil } +func (s *AuthStorage) SetIntrospectionFromToken(ctx context.Context, introspect oidc.IntrospectionResponse, tokenID, subject, clientID string) error { + if err := s.SetUserinfoFromScopes(ctx, introspect, "", "", []string{}); err != nil { + return err + } + introspect.SetClientID(a.ClientID) + return nil +} + +func (s *AuthStorage) ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) { + return scope, nil +} + type ConfClient struct { applicationType op.ApplicationType - authMethod op.AuthMethod + authMethod oidc.AuthMethod responseTypes []oidc.ResponseType ID string accessTokenType op.AccessTokenType @@ -262,7 +273,7 @@ func (c *ConfClient) ApplicationType() op.ApplicationType { return c.applicationType } -func (c *ConfClient) AuthMethod() op.AuthMethod { +func (c *ConfClient) AuthMethod() oidc.AuthMethod { return c.authMethod } diff --git a/pkg/client/client.go b/pkg/client/client.go new file mode 100644 index 0000000..b2b815e --- /dev/null +++ b/pkg/client/client.go @@ -0,0 +1,90 @@ +package client + +import ( + "net/http" + "reflect" + "strings" + "time" + + "github.com/gorilla/schema" + "golang.org/x/oauth2" + "gopkg.in/square/go-jose.v2" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +var ( + Encoder = func() utils.Encoder { + e := schema.NewEncoder() + e.RegisterEncoder(oidc.Scopes{}, func(value reflect.Value) string { + return value.Interface().(oidc.Scopes).Encode() + }) + return e + }() +) + +//Discover calls the discovery endpoint of the provided issuer and returns its configuration +func Discover(issuer string, httpClient *http.Client) (*oidc.DiscoveryConfiguration, error) { + wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint + req, err := http.NewRequest("GET", wellKnown, nil) + if err != nil { + return nil, err + } + discoveryConfig := new(oidc.DiscoveryConfiguration) + err = utils.HttpRequest(httpClient, req, &discoveryConfig) + if err != nil { + return nil, err + } + return discoveryConfig, nil +} + +type tokenEndpointCaller interface { + TokenEndpoint() string + HttpClient() *http.Client +} + +func CallTokenEndpoint(request interface{}, caller tokenEndpointCaller) (newToken *oauth2.Token, err error) { + return callTokenEndpoint(request, nil, caller) +} + +func callTokenEndpoint(request interface{}, authFn interface{}, caller tokenEndpointCaller) (newToken *oauth2.Token, err error) { + req, err := utils.FormRequest(caller.TokenEndpoint(), request, Encoder, authFn) + if err != nil { + return nil, err + } + tokenRes := new(oidc.AccessTokenResponse) + if err := utils.HttpRequest(caller.HttpClient(), req, &tokenRes); err != nil { + return nil, err + } + return &oauth2.Token{ + AccessToken: tokenRes.AccessToken, + TokenType: tokenRes.TokenType, + RefreshToken: tokenRes.RefreshToken, + Expiry: time.Now().UTC().Add(time.Duration(tokenRes.ExpiresIn) * time.Second), + }, nil +} + +func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) { + privateKey, err := utils.BytesToPrivateKey(key) + if err != nil { + return nil, err + } + signingKey := jose.SigningKey{ + Algorithm: jose.RS256, + Key: &jose.JSONWebKey{Key: privateKey, KeyID: keyID}, + } + return jose.NewSigner(signingKey, &jose.SignerOptions{}) +} + +func SignedJWTProfileAssertion(clientID string, audience []string, expiration time.Duration, signer jose.Signer) (string, error) { + iat := time.Now() + exp := iat.Add(expiration) + return utils.Sign(&oidc.JWTTokenRequest{ + Issuer: clientID, + Subject: clientID, + Audience: audience, + ExpiresAt: oidc.Time(exp), + IssuedAt: oidc.Time(iat), + }, signer) +} diff --git a/pkg/client/jwt_profile.go b/pkg/client/jwt_profile.go new file mode 100644 index 0000000..8095588 --- /dev/null +++ b/pkg/client/jwt_profile.go @@ -0,0 +1,30 @@ +package client + +import ( + "context" + "net/url" + + "golang.org/x/oauth2" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +//JWTProfileExchange handles the oauth2 jwt profile exchange +func JWTProfileExchange(ctx context.Context, jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, caller tokenEndpointCaller) (*oauth2.Token, error) { + return CallTokenEndpoint(jwtProfileGrantRequest, caller) +} + +func ClientAssertionCodeOptions(assertion string) []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam("client_assertion", assertion), + oauth2.SetAuthURLParam("client_assertion_type", oidc.ClientAssertionTypeJWTAssertion), + } +} + +func ClientAssertionFormAuthorization(assertion string) utils.FormAuthorization { + return func(values url.Values) { + values.Set("client_assertion", assertion) + values.Set("client_assertion_type", oidc.ClientAssertionTypeJWTAssertion) + } +} diff --git a/pkg/client/key.go b/pkg/client/key.go new file mode 100644 index 0000000..f89a2b4 --- /dev/null +++ b/pkg/client/key.go @@ -0,0 +1,40 @@ +package client + +import ( + "encoding/json" + "io/ioutil" +) + +const ( + serviceAccountKey = "serviceaccount" + applicationKey = "application" +) + +type keyFile struct { + Type string `json:"type"` // serviceaccount or application + KeyID string `json:"keyId"` + Key string `json:"key"` + Issuer string `json:"issuer"` //not yet in file + + //serviceaccount + UserID string `json:"userId"` + + //application + ClientID string `json:"clientId"` +} + +func ConfigFromKeyFile(path string) (*keyFile, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + return ConfigFromKeyFileData(data) +} + +func ConfigFromKeyFileData(data []byte) (*keyFile, error) { + var f keyFile + if err := json.Unmarshal(data, &f); err != nil { + return nil, err + } + return &f, nil +} diff --git a/pkg/client/profile/jwt_profile.go b/pkg/client/profile/jwt_profile.go new file mode 100644 index 0000000..46a0fe9 --- /dev/null +++ b/pkg/client/profile/jwt_profile.go @@ -0,0 +1,93 @@ +package profile + +import ( + "net/http" + "time" + + "golang.org/x/oauth2" + "gopkg.in/square/go-jose.v2" + + "github.com/caos/oidc/pkg/client" + "github.com/caos/oidc/pkg/oidc" +) + +//jwtProfileTokenSource implement the oauth2.TokenSource +//it will request a token using the OAuth2 JWT Profile Grant +//therefore sending an `assertion` by singing a JWT with the provided private key +type jwtProfileTokenSource struct { + clientID string + audience []string + signer jose.Signer + scopes []string + httpClient *http.Client + tokenEndpoint string +} + +func NewJWTProfileTokenSourceFromKeyFile(issuer, keyPath string, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) { + keyData, err := client.ConfigFromKeyFile(keyPath) + if err != nil { + return nil, err + } + return NewJWTProfileTokenSource(issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...) +} + +func NewJWTProfileTokenSourceFromKeyFileData(issuer string, data []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) { + keyData, err := client.ConfigFromKeyFileData(data) + if err != nil { + return nil, err + } + return NewJWTProfileTokenSource(issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...) +} + +func NewJWTProfileTokenSource(issuer, clientID, keyID string, key []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) { + signer, err := client.NewSignerFromPrivateKeyByte(key, keyID) + if err != nil { + return nil, err + } + source := &jwtProfileTokenSource{ + clientID: clientID, + audience: []string{issuer}, + signer: signer, + scopes: scopes, + httpClient: http.DefaultClient, + } + for _, opt := range options { + opt(source) + } + if source.tokenEndpoint == "" { + config, err := client.Discover(issuer, source.httpClient) + if err != nil { + return nil, err + } + source.tokenEndpoint = config.TokenEndpoint + } + return source, nil +} + +func WithHTTPClient(client *http.Client) func(*jwtProfileTokenSource) { + return func(source *jwtProfileTokenSource) { + source.httpClient = client + } +} + +func WithStaticTokenEndpoint(issuer, tokenEndpoint string) func(*jwtProfileTokenSource) { + return func(source *jwtProfileTokenSource) { + source.tokenEndpoint = tokenEndpoint + } +} + +func (j *jwtProfileTokenSource) TokenEndpoint() string { + return j.tokenEndpoint +} + +func (j *jwtProfileTokenSource) HttpClient() *http.Client { + return j.httpClient +} + +func (j *jwtProfileTokenSource) Token() (*oauth2.Token, error) { + assertion, err := client.SignedJWTProfileAssertion(j.clientID, j.audience, time.Hour, j.signer) + if err != nil { + return nil, err + } + return client.JWTProfileExchange(nil, oidc.NewJWTProfileGrantRequest(assertion, j.scopes...), j) +} diff --git a/pkg/rp/cli/cli.go b/pkg/client/rp/cli/cli.go similarity index 67% rename from pkg/rp/cli/cli.go rename to pkg/client/rp/cli/cli.go index 4b00ba0..6cbb364 100644 --- a/pkg/rp/cli/cli.go +++ b/pkg/client/rp/cli/cli.go @@ -4,8 +4,8 @@ import ( "context" "net/http" + "github.com/caos/oidc/pkg/client/rp" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/rp" "github.com/caos/oidc/pkg/utils" ) @@ -13,7 +13,7 @@ const ( loginPath = "/login" ) -func CodeFlow(relayingParty rp.RelayingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens { +func CodeFlow(relyingParty rp.RelyingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -24,8 +24,8 @@ func CodeFlow(relayingParty rp.RelayingParty, callbackPath, port string, statePr msg = msg + "

You are authenticated and can now return to the CLI.

" w.Write([]byte(msg)) } - http.Handle(loginPath, rp.AuthURLHandler(stateProvider, relayingParty)) - http.Handle(callbackPath, rp.CodeExchangeHandler(callback, relayingParty)) + http.Handle(loginPath, rp.AuthURLHandler(stateProvider, relyingParty)) + http.Handle(callbackPath, rp.CodeExchangeHandler(callback, relyingParty)) utils.StartServer(ctx, port) diff --git a/pkg/rp/delegation.go b/pkg/client/rp/delegation.go similarity index 100% rename from pkg/rp/delegation.go rename to pkg/client/rp/delegation.go diff --git a/pkg/rp/jwks.go b/pkg/client/rp/jwks.go similarity index 100% rename from pkg/rp/jwks.go rename to pkg/client/rp/jwks.go diff --git a/pkg/rp/mock/generate.go b/pkg/client/rp/mock/generate.go similarity index 100% rename from pkg/rp/mock/generate.go rename to pkg/client/rp/mock/generate.go diff --git a/pkg/rp/mock/verifier.mock.go b/pkg/client/rp/mock/verifier.mock.go similarity index 94% rename from pkg/rp/mock/verifier.mock.go rename to pkg/client/rp/mock/verifier.mock.go index acd7d77..08cf77f 100644 --- a/pkg/rp/mock/verifier.mock.go +++ b/pkg/client/rp/mock/verifier.mock.go @@ -5,10 +5,12 @@ package mock import ( - context "context" - oidc "github.com/caos/oidc/pkg/oidc" - gomock "github.com/golang/mock/gomock" - reflect "reflect" + "context" + "reflect" + + "github.com/golang/mock/gomock" + + "github.com/caos/oidc/pkg/oidc" ) // MockVerifier is a mock of Verifier interface diff --git a/pkg/rp/relaying_party.go b/pkg/client/rp/relaying_party.go similarity index 72% rename from pkg/rp/relaying_party.go rename to pkg/client/rp/relaying_party.go index 6807221..528f554 100644 --- a/pkg/rp/relaying_party.go +++ b/pkg/client/rp/relaying_party.go @@ -4,43 +4,32 @@ import ( "context" "errors" "net/http" - "net/url" - "reflect" "strings" "time" "github.com/google/uuid" - "github.com/gorilla/schema" - - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/oidc/grants" - "github.com/caos/oidc/pkg/utils" - "golang.org/x/oauth2" + "gopkg.in/square/go-jose.v2" + + "github.com/caos/oidc/pkg/client" + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" ) const ( - idTokenKey = "id_token" - stateParam = "state" - pkceCode = "pkce" - jwtProfileKey = "urn:ietf:params:oauth:grant-type:jwt-bearer" + idTokenKey = "id_token" + stateParam = "state" + pkceCode = "pkce" ) -var ( - encoder = func() utils.Encoder { - e := schema.NewEncoder() - e.RegisterEncoder(oidc.Scopes{}, func(value reflect.Value) string { - return value.Interface().(oidc.Scopes).Encode() - }) - return e - }() -) - -//RelayingParty declares the minimal interface for oidc clients -type RelayingParty interface { +//RelyingParty declares the minimal interface for oidc clients +type RelyingParty interface { //OAuthConfig returns the oauth2 Config OAuthConfig() *oauth2.Config + //Issuer returns the issuer of the oidc config + Issuer() string + //IsPKCE returns if authorization is done using `Authorization Code Flow with Proof Key for Code Exchange (PKCE)` IsPKCE() bool @@ -53,10 +42,16 @@ type RelayingParty interface { //IsOAuth2Only specifies whether relaying party handles only oauth2 or oidc calls IsOAuth2Only() bool + //Signer is used if the relaying party uses the JWT Profile + Signer() jose.Signer + + //UserinfoEndpoint returns the userinfo + UserinfoEndpoint() string + //IDTokenVerifier returns the verifier interface used for oidc id_token verification IDTokenVerifier() IDTokenVerifier - //ErrorHandler returns the handler used for callback errors + ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) } @@ -68,7 +63,7 @@ var ( } ) -type relayingParty struct { +type relyingParty struct { issuer string endpoints Endpoints oauthConfig *oauth2.Config @@ -77,68 +72,83 @@ type relayingParty struct { httpClient *http.Client cookieHandler *utils.CookieHandler - errorHandler func(http.ResponseWriter, *http.Request, string, string, string) + errorHandler func(http.ResponseWriter, *http.Request, string, string, string) idTokenVerifier IDTokenVerifier verifierOpts []VerifierOption + signer jose.Signer } -func (rp *relayingParty) OAuthConfig() *oauth2.Config { +func (rp *relyingParty) OAuthConfig() *oauth2.Config { return rp.oauthConfig } -func (rp *relayingParty) IsPKCE() bool { +func (rp *relyingParty) Issuer() string { + return rp.issuer +} + +func (rp *relyingParty) IsPKCE() bool { return rp.pkce } -func (rp *relayingParty) CookieHandler() *utils.CookieHandler { +func (rp *relyingParty) CookieHandler() *utils.CookieHandler { return rp.cookieHandler } -func (rp *relayingParty) HttpClient() *http.Client { +func (rp *relyingParty) HttpClient() *http.Client { return rp.httpClient } -func (rp *relayingParty) IsOAuth2Only() bool { +func (rp *relyingParty) IsOAuth2Only() bool { return rp.oauth2Only } -func (rp *relayingParty) IDTokenVerifier() IDTokenVerifier { +func (rp *relyingParty) Signer() jose.Signer { + return rp.signer +} + +func (rp *relyingParty) UserinfoEndpoint() string { + return rp.endpoints.UserinfoURL +} + +func (rp *relyingParty) IDTokenVerifier() IDTokenVerifier { if rp.idTokenVerifier == nil { rp.idTokenVerifier = NewIDTokenVerifier(rp.issuer, rp.oauthConfig.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...) } return rp.idTokenVerifier } -func (rp *relayingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) { +func (rp *relyingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) { if rp.errorHandler == nil { rp.errorHandler = DefaultErrorHandler } return rp.errorHandler } -//NewRelayingPartyOAuth creates an (OAuth2) RelayingParty with the given +//NewRelyingPartyOAuth creates an (OAuth2) RelyingParty with the given //OAuth2 Config and possible configOptions //it will use the AuthURL and TokenURL set in config -func NewRelayingPartyOAuth(config *oauth2.Config, options ...Option) (RelayingParty, error) { - rp := &relayingParty{ +func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingParty, error) { + rp := &relyingParty{ oauthConfig: config, httpClient: utils.DefaultHTTPClient, oauth2Only: true, } for _, optFunc := range options { - optFunc(rp) + if err := optFunc(rp); err != nil { + return nil, err + } } return rp, nil } -//NewRelayingPartyOIDC creates an (OIDC) RelayingParty with the given +//NewRelyingPartyOIDC creates an (OIDC) RelyingParty with the given //issuer, clientID, clientSecret, redirectURI, scopes and possible configOptions //it will run discovery on the provided issuer and use the found endpoints -func NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelayingParty, error) { - rp := &relayingParty{ +func NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelyingParty, error) { + rp := &relyingParty{ issuer: issuer, oauthConfig: &oauth2.Config{ ClientID: clientID, @@ -151,7 +161,9 @@ func NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, sc } for _, optFunc := range options { - optFunc(rp) + if err := optFunc(rp); err != nil { + return nil, err + } } endpoints, err := Discover(rp.issuer, rp.httpClient) @@ -165,12 +177,13 @@ func NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, sc } //DefaultRPOpts is the type for providing dynamic options to the DefaultRP -type Option func(*relayingParty) +type Option func(*relyingParty) error //WithCookieHandler set a `CookieHandler` for securing the various redirects func WithCookieHandler(cookieHandler *utils.CookieHandler) Option { - return func(rp *relayingParty) { + return func(rp *relyingParty) error { rp.cookieHandler = cookieHandler + return nil } } @@ -178,32 +191,49 @@ func WithCookieHandler(cookieHandler *utils.CookieHandler) Option { //it also sets a `CookieHandler` for securing the various redirects //and exchanging the code challenge func WithPKCE(cookieHandler *utils.CookieHandler) Option { - return func(rp *relayingParty) { + return func(rp *relyingParty) error { rp.pkce = true rp.cookieHandler = cookieHandler + return nil } } //WithHTTPClient provides the ability to set an http client to be used for the relaying party and verifier func WithHTTPClient(client *http.Client) Option { - return func(rp *relayingParty) { + return func(rp *relyingParty) error { rp.httpClient = client + return nil } } func WithErrorHandler(errorHandler ErrorHandler) Option { - return func(rp *relayingParty) { + return func(rp *relyingParty) error { rp.errorHandler = errorHandler + return nil } } func WithVerifierOpts(opts ...VerifierOption) Option { - return func(rp *relayingParty) { + return func(rp *relyingParty) error { rp.verifierOpts = opts + return nil + } +} + +func WithClientKey(path string) Option { + return func(rp *relyingParty) error { + config, err := client.ConfigFromKeyFile(path) + if err != nil { + return err + } + rp.signer, err = client.NewSignerFromPrivateKeyByte([]byte(config.Key), config.KeyID) + return err } } //Discover calls the discovery endpoint of the provided issuer and returns the found endpoints +// +//deprecated: use client.Discover func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint req, err := http.NewRequest("GET", wellKnown, nil) @@ -220,7 +250,7 @@ func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { //AuthURL returns the auth request url //(wrapping the oauth2 `AuthCodeURL`) -func AuthURL(state string, rp RelayingParty, opts ...AuthURLOpt) string { +func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string { authOpts := make([]oauth2.AuthCodeOption, 0) for _, opt := range opts { authOpts = append(authOpts, opt()...) @@ -230,7 +260,7 @@ func AuthURL(state string, rp RelayingParty, opts ...AuthURLOpt) string { //AuthURLHandler extends the `AuthURL` method with a http redirect handler //including handling setting cookie for secure `state` transfer -func AuthURLHandler(stateFn func() string, rp RelayingParty) http.HandlerFunc { +func AuthURLHandler(stateFn func() string, rp RelyingParty) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { opts := make([]AuthURLOpt, 0) state := stateFn() @@ -251,7 +281,7 @@ func AuthURLHandler(stateFn func() string, rp RelayingParty) http.HandlerFunc { } //GenerateAndStoreCodeChallenge generates a PKCE code challenge and stores its verifier into a secure cookie -func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelayingParty) (string, error) { +func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (string, error) { codeVerifier := uuid.New().String() if err := rp.CookieHandler().SetCookie(w, pkceCode, codeVerifier); err != nil { return "", err @@ -261,7 +291,7 @@ func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelayingParty) (str //CodeExchange handles the oauth2 code exchange, extracting and validating the id_token //returning it parsed together with the oauth2 tokens (access, refresh) -func CodeExchange(ctx context.Context, code string, rp RelayingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) { +func CodeExchange(ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) { ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient()) codeOpts := make([]oauth2.AuthCodeOption, 0) for _, opt := range opts { @@ -293,7 +323,7 @@ func CodeExchange(ctx context.Context, code string, rp RelayingParty, opts ...Co //CodeExchangeHandler extends the `CodeExchange` method with a http handler //including cookie handling for secure `state` transfer //and optional PKCE code verifier checking -func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string), rp RelayingParty) http.HandlerFunc { +func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string), rp RelyingParty) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { state, err := tryReadStateCookie(w, r, rp) if err != nil { @@ -314,6 +344,14 @@ func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc } codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier)) } + if rp.Signer() != nil { + assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer()}, time.Hour, rp.Signer()) + if err != nil { + http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized) + return + } + codeOpts = append(codeOpts, WithClientAssertionJWT(assertion)) + } tokens, err := CodeExchange(r.Context(), params.Get("code"), rp, codeOpts...) if err != nil { http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) @@ -323,51 +361,21 @@ func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc } } -//ClientCredentials is the `RelayingParty` interface implementation -//handling the oauth2 client credentials grant -func ClientCredentials(ctx context.Context, rp RelayingParty, scopes ...string) (newToken *oauth2.Token, err error) { - return CallTokenEndpointAuthorized(grants.ClientCredentialsGrantBasic(scopes...), rp) -} - -func CallTokenEndpointAuthorized(request interface{}, rp RelayingParty) (newToken *oauth2.Token, err error) { - config := rp.OAuthConfig() - var fn interface{} = utils.AuthorizeBasic(config.ClientID, config.ClientSecret) - if config.Endpoint.AuthStyle == oauth2.AuthStyleInParams { - fn = func(form url.Values) { - form.Set("client_id", config.ClientID) - form.Set("client_secret", config.ClientSecret) - } - } - return callTokenEndpoint(request, fn, rp) -} - -func CallTokenEndpoint(request interface{}, rp RelayingParty) (newToken *oauth2.Token, err error) { - return callTokenEndpoint(request, nil, rp) -} - -func callTokenEndpoint(request interface{}, authFn interface{}, rp RelayingParty) (newToken *oauth2.Token, err error) { - req, err := utils.FormRequest(rp.OAuthConfig().Endpoint.TokenURL, request, encoder, authFn) +//Userinfo will call the OIDC Userinfo Endpoint with the provided token +func Userinfo(token string, rp RelyingParty) (oidc.UserInfo, error) { + req, err := http.NewRequest("GET", rp.UserinfoEndpoint(), nil) if err != nil { return nil, err } - var tokenRes struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - ExpiresIn int64 `json:"expires_in"` - RefreshToken string `json:"refresh_token"` - } - if err := utils.HttpRequest(rp.HttpClient(), req, &tokenRes); err != nil { + req.Header.Set("authorization", token) + userinfo := oidc.NewUserInfo() + if err := utils.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil { return nil, err } - return &oauth2.Token{ - AccessToken: tokenRes.AccessToken, - TokenType: tokenRes.TokenType, - RefreshToken: tokenRes.RefreshToken, - Expiry: time.Now().UTC().Add(time.Duration(tokenRes.ExpiresIn) * time.Second), - }, nil + return userinfo, nil } -func trySetStateCookie(w http.ResponseWriter, state string, rp RelayingParty) error { +func trySetStateCookie(w http.ResponseWriter, state string, rp RelyingParty) error { if rp.CookieHandler() != nil { if err := rp.CookieHandler().SetCookie(w, stateParam, state); err != nil { return err @@ -376,7 +384,7 @@ func trySetStateCookie(w http.ResponseWriter, state string, rp RelayingParty) er return nil } -func tryReadStateCookie(w http.ResponseWriter, r *http.Request, rp RelayingParty) (state string, err error) { +func tryReadStateCookie(w http.ResponseWriter, r *http.Request, rp RelyingParty) (state string, err error) { if rp.CookieHandler() == nil { return r.FormValue(stateParam), nil } @@ -388,7 +396,7 @@ func tryReadStateCookie(w http.ResponseWriter, r *http.Request, rp RelayingParty return state, nil } -type OptionFunc func(RelayingParty) +type OptionFunc func(RelyingParty) type Endpoints struct { oauth2.Endpoint @@ -439,3 +447,10 @@ func WithCodeVerifier(codeVerifier string) CodeExchangeOpt { return []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("code_verifier", codeVerifier)} } } + +//WithClientAssertionJWT sets the `client_assertion` param in the token request +func WithClientAssertionJWT(clientAssertion string) CodeExchangeOpt { + return func() []oauth2.AuthCodeOption { + return client.ClientAssertionCodeOptions(clientAssertion) + } +} diff --git a/pkg/client/rp/tockenexchange.go b/pkg/client/rp/tockenexchange.go new file mode 100644 index 0000000..d5056ae --- /dev/null +++ b/pkg/client/rp/tockenexchange.go @@ -0,0 +1,27 @@ +package rp + +import ( + "context" + + "golang.org/x/oauth2" + + "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" +) + +//TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange` +type TokenExchangeRP interface { + RelyingParty + + //TokenExchange implement the `Token Exchange Grant` exchanging some token for an other + TokenExchange(context.Context, *tokenexchange.TokenExchangeRequest) (*oauth2.Token, error) +} + +//DelegationTokenExchangeRP extends the `TokenExchangeRP` interface +//for the specific `delegation token` request +type DelegationTokenExchangeRP interface { + TokenExchangeRP + + //DelegationTokenExchange implement the `Token Exchange Grant` + //providing an access token in request for a `delegation` token for a given resource / audience + DelegationTokenExchange(context.Context, string, ...tokenexchange.TokenExchangeOption) (*oauth2.Token, error) +} diff --git a/pkg/rp/verifier.go b/pkg/client/rp/verifier.go similarity index 92% rename from pkg/rp/verifier.go rename to pkg/client/rp/verifier.go index a156f6d..1f45ca8 100644 --- a/pkg/rp/verifier.go +++ b/pkg/client/rp/verifier.go @@ -214,13 +214,3 @@ func (i *idTokenVerifier) ACR() oidc.ACRVerifier { func (i *idTokenVerifier) MaxAge() time.Duration { return i.maxAge } - -//deprecated: Use IDTokenVerifier (or oidc.Verifier) -type Verifier interface { - - //Verify checks the access_token and id_token and returns the `id token claims` - Verify(ctx context.Context, accessToken, idTokenString string) (*oidc.IDTokenClaims, error) - - //VerifyIDToken checks the id_token only and returns its `id token claims` - VerifyIDToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error) -} diff --git a/pkg/client/rs/resource_server.go b/pkg/client/rs/resource_server.go new file mode 100644 index 0000000..f5dbe69 --- /dev/null +++ b/pkg/client/rs/resource_server.go @@ -0,0 +1,123 @@ +package rs + +import ( + "context" + "errors" + "net/http" + "time" + + "github.com/caos/oidc/pkg/client" + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +type ResourceServer interface { + IntrospectionURL() string + HttpClient() *http.Client + AuthFn() (interface{}, error) +} + +type resourceServer struct { + issuer string + tokenURL string + introspectURL string + httpClient *http.Client + authFn func() (interface{}, error) +} + +func (r *resourceServer) IntrospectionURL() string { + return r.introspectURL +} + +func (r *resourceServer) HttpClient() *http.Client { + return r.httpClient +} + +func (r *resourceServer) AuthFn() (interface{}, error) { + return r.authFn() +} + +func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option Option) (ResourceServer, error) { + authorizer := func() (interface{}, error) { + return utils.AuthorizeBasic(clientID, clientSecret), nil + } + return newResourceServer(issuer, authorizer, option) +} +func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...Option) (ResourceServer, error) { + signer, err := client.NewSignerFromPrivateKeyByte(key, keyID) + if err != nil { + return nil, err + } + authorizer := func() (interface{}, error) { + assertion, err := client.SignedJWTProfileAssertion(clientID, []string{issuer}, time.Hour, signer) + if err != nil { + return nil, err + } + return client.ClientAssertionFormAuthorization(assertion), nil + } + return newResourceServer(issuer, authorizer, options...) +} + +func newResourceServer(issuer string, authorizer func() (interface{}, error), options ...Option) (*resourceServer, error) { + rs := &resourceServer{ + issuer: issuer, + httpClient: utils.DefaultHTTPClient, + } + for _, optFunc := range options { + optFunc(rs) + } + if rs.introspectURL == "" || rs.tokenURL == "" { + config, err := client.Discover(rs.issuer, rs.httpClient) + if err != nil { + return nil, err + } + rs.tokenURL = config.TokenEndpoint + rs.introspectURL = config.IntrospectionEndpoint + } + if rs.introspectURL == "" || rs.tokenURL == "" { + return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url") + } + rs.authFn = authorizer + return rs, nil +} + +func NewResourceServerFromKeyFile(issuer, path string, options ...Option) (ResourceServer, error) { + c, err := client.ConfigFromKeyFile(path) + if err != nil { + return nil, err + } + return NewResourceServerJWTProfile(issuer, c.ClientID, c.KeyID, []byte(c.Key), options...) +} + +type Option func(*resourceServer) + +//WithClient provides the ability to set an http client to be used for the resource server +func WithClient(client *http.Client) Option { + return func(server *resourceServer) { + server.httpClient = client + } +} + +//WithStaticEndpoints provides the ability to set static token and introspect URL +func WithStaticEndpoints(tokenURL, introspectURL string) Option { + return func(server *resourceServer) { + server.tokenURL = tokenURL + server.introspectURL = introspectURL + } +} + +func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.IntrospectionResponse, error) { + authFn, err := rp.AuthFn() + if err != nil { + return nil, err + } + req, err := utils.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, client.Encoder, authFn) + if err != nil { + return nil, err + } + resp := oidc.NewIntrospectionResponse() + if err := utils.HttpRequest(rp.HttpClient(), req, resp); err != nil { + return nil, err + } + return resp, nil +} diff --git a/pkg/oidc/code_challenge.go b/pkg/oidc/code_challenge.go index 44a0499..9c4c8a3 100644 --- a/pkg/oidc/code_challenge.go +++ b/pkg/oidc/code_challenge.go @@ -24,7 +24,7 @@ func NewSHACodeChallenge(code string) string { func VerifyCodeChallenge(c *CodeChallenge, codeVerifier string) bool { if c == nil { - return false //TODO: ? + return false } if c.Method == CodeChallengeMethodS256 { codeVerifier = NewSHACodeChallenge(codeVerifier) diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go index 9333ca9..acab578 100644 --- a/pkg/oidc/discovery.go +++ b/pkg/oidc/discovery.go @@ -1,25 +1,68 @@ package oidc +import ( + "golang.org/x/text/language" +) + const ( DiscoveryEndpoint = "/.well-known/openid-configuration" ) type DiscoveryConfiguration struct { - Issuer string `json:"issuer,omitempty"` - AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` - TokenEndpoint string `json:"token_endpoint,omitempty"` - IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` - UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` - EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` - CheckSessionIframe string `json:"check_session_iframe,omitempty"` - JwksURI string `json:"jwks_uri,omitempty"` - ScopesSupported []string `json:"scopes_supported,omitempty"` - ResponseTypesSupported []string `json:"response_types_supported,omitempty"` - ResponseModesSupported []string `json:"response_modes_supported,omitempty"` - GrantTypesSupported []string `json:"grant_types_supported,omitempty"` - SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` - IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` - TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"` - CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported,omitempty"` - ClaimsSupported []string `json:"claims_supported,omitempty"` + Issuer string `json:"issuer,omitempty"` + AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` + TokenEndpoint string `json:"token_endpoint,omitempty"` + IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` + UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` + RevocationEndpoint string `json:"revocation_endpoint,omitempty"` + EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` + CheckSessionIframe string `json:"check_session_iframe,omitempty"` + JwksURI string `json:"jwks_uri,omitempty"` + ScopesSupported []string `json:"scopes_supported,omitempty"` + ResponseTypesSupported []string `json:"response_types_supported,omitempty"` + ResponseModesSupported []string `json:"response_modes_supported,omitempty"` + GrantTypesSupported []GrantType `json:"grant_types_supported,omitempty"` + ACRValuesSupported []string `json:"acr_values_supported,omitempty"` + SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` + IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` + IDTokenEncryptionAlgValuesSupported []string `json:"id_token_encryption_alg_values_supported,omitempty"` + IDTokenEncryptionEncValuesSupported []string `json:"id_token_encryption_enc_values_supported,omitempty"` + UserinfoSigningAlgValuesSupported []string `json:"userinfo_signing_alg_values_supported,omitempty"` + UserinfoEncryptionAlgValuesSupported []string `json:"userinfo_encryption_alg_values_supported,omitempty"` + UserinfoEncryptionEncValuesSupported []string `json:"userinfo_encryption_enc_values_supported,omitempty"` + RequestObjectSigningAlgValuesSupported []string `json:"request_object_signing_alg_values_supported,omitempty"` + RequestObjectEncryptionAlgValuesSupported []string `json:"request_object_encryption_alg_values_supported,omitempty"` + RequestObjectEncryptionEncValuesSupported []string `json:"request_object_encryption_enc_values_supported,omitempty"` + TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"` + TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"` + RevocationEndpointAuthMethodsSupported []AuthMethod `json:"revocation_endpoint_auth_methods_supported,omitempty"` + RevocationEndpointAuthSigningAlgValuesSupported []string `json:"revocation_endpoint_auth_signing_alg_values_supported,omitempty"` + IntrospectionEndpointAuthMethodsSupported []AuthMethod `json:"introspection_endpoint_auth_methods_supported,omitempty"` + IntrospectionEndpointAuthSigningAlgValuesSupported []string `json:"introspection_endpoint_auth_signing_alg_values_supported,omitempty"` + DisplayValuesSupported []Display `json:"display_values_supported,omitempty"` + ClaimTypesSupported []string `json:"claim_types_supported,omitempty"` + ClaimsSupported []string `json:"claims_supported,omitempty"` + ClaimsParameterSupported bool `json:"claims_parameter_supported,omitempty"` + CodeChallengeMethodsSupported []CodeChallengeMethod `json:"code_challenge_methods_supported,omitempty"` + ServiceDocumentation string `json:"service_documentation,omitempty"` + ClaimsLocalesSupported []language.Tag `json:"claims_locales_supported,omitempty"` + UILocalesSupported []language.Tag `json:"ui_locales_supported,omitempty"` + RequestParameterSupported bool `json:"request_parameter_supported,omitempty"` + RequestURIParameterSupported bool `json:"request_uri_parameter_supported"` //no omitempty because: If omitted, the default value is true + RequireRequestURIRegistration bool `json:"require_request_uri_registration,omitempty"` + OPPolicyURI string `json:"op_policy_uri,omitempty"` + OPTermsOfServiceURI string `json:"op_tos_uri,omitempty"` } + +type AuthMethod string + +const ( + AuthMethodBasic AuthMethod = "client_secret_basic" + AuthMethodPost AuthMethod = "client_secret_post" + AuthMethodNone AuthMethod = "none" + AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt" +) + +const ( + GrantTypeImplicit GrantType = "implicit" +) diff --git a/pkg/oidc/grants/tokenexchange/tokenexchange.go b/pkg/oidc/grants/tokenexchange/tokenexchange.go index 5cb6e79..02a9808 100644 --- a/pkg/oidc/grants/tokenexchange/tokenexchange.go +++ b/pkg/oidc/grants/tokenexchange/tokenexchange.go @@ -1,9 +1,5 @@ package tokenexchange -import ( - "github.com/caos/oidc/pkg/oidc" -) - const ( AccessTokenType = "urn:ietf:params:oauth:token-type:access_token" RefreshTokenType = "urn:ietf:params:oauth:token-type:refresh_token" @@ -26,22 +22,6 @@ type TokenExchangeRequest struct { requestedTokenType string `schema:"requested_token_type"` } -type JWTProfileRequest struct { - Assertion string `schema:"assertion"` - Scope oidc.Scopes `schema:"scope"` - GrantType oidc.GrantType `schema:"grant_type"` -} - -//ClientCredentialsGrantBasic creates an oauth2 `Client Credentials` Grant -//sneding client_id and client_secret as basic auth header -func NewJWTProfileRequest(assertion string, scopes ...string) *JWTProfileRequest { - return &JWTProfileRequest{ - GrantType: oidc.GrantTypeBearer, - Assertion: assertion, - Scope: scopes, - } -} - func NewTokenExchangeRequest(subjectToken, subjectTokenType string, opts ...TokenExchangeOption) *TokenExchangeRequest { t := &TokenExchangeRequest{ grantType: TokenExchangeGrantType, diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go new file mode 100644 index 0000000..a2176aa --- /dev/null +++ b/pkg/oidc/introspection.go @@ -0,0 +1,276 @@ +package oidc + +import ( + "encoding/json" + "fmt" + "time" + + "golang.org/x/text/language" +) + +type IntrospectionRequest struct { + Token string `schema:"token"` +} + +type ClientAssertionParams struct { + ClientAssertion string `schema:"client_assertion"` + ClientAssertionType string `schema:"client_assertion_type"` +} + +type IntrospectionResponse interface { + UserInfoSetter + SetActive(bool) + IsActive() bool + SetScopes(scopes Scopes) + SetClientID(id string) +} + +func NewIntrospectionResponse() IntrospectionResponse { + return &introspectionResponse{} +} + +type introspectionResponse struct { + Active bool `json:"active"` + Scope Scopes `json:"scope,omitempty"` + ClientID string `json:"client_id,omitempty"` + Subject string `json:"sub,omitempty"` + userInfoProfile + userInfoEmail + userInfoPhone + + Address UserInfoAddress `json:"address,omitempty"` + claims map[string]interface{} +} + +func (u *introspectionResponse) IsActive() bool { + return u.Active +} + +func (u *introspectionResponse) SetScopes(scope Scopes) { + u.Scope = scope +} + +func (u *introspectionResponse) SetClientID(id string) { + u.ClientID = id +} + +func (u *introspectionResponse) GetSubject() string { + return u.Subject +} + +func (u *introspectionResponse) GetName() string { + return u.Name +} + +func (u *introspectionResponse) GetGivenName() string { + return u.GivenName +} + +func (u *introspectionResponse) GetFamilyName() string { + return u.FamilyName +} + +func (u *introspectionResponse) GetMiddleName() string { + return u.MiddleName +} + +func (u *introspectionResponse) GetNickname() string { + return u.Nickname +} + +func (u *introspectionResponse) GetProfile() string { + return u.Profile +} + +func (u *introspectionResponse) GetPicture() string { + return u.Picture +} + +func (u *introspectionResponse) GetWebsite() string { + return u.Website +} + +func (u *introspectionResponse) GetGender() Gender { + return u.Gender +} + +func (u *introspectionResponse) GetBirthdate() string { + return u.Birthdate +} + +func (u *introspectionResponse) GetZoneinfo() string { + return u.Zoneinfo +} + +func (u *introspectionResponse) GetLocale() language.Tag { + return u.Locale +} + +func (u *introspectionResponse) GetPreferredUsername() string { + return u.PreferredUsername +} + +func (u *introspectionResponse) GetEmail() string { + return u.Email +} + +func (u *introspectionResponse) IsEmailVerified() bool { + return u.EmailVerified +} + +func (u *introspectionResponse) GetPhoneNumber() string { + return u.PhoneNumber +} + +func (u *introspectionResponse) IsPhoneNumberVerified() bool { + return u.PhoneNumberVerified +} + +func (u *introspectionResponse) GetAddress() UserInfoAddress { + return u.Address +} + +func (u *introspectionResponse) GetClaim(key string) interface{} { + return u.claims[key] +} + +func (u *introspectionResponse) SetActive(active bool) { + u.Active = active +} + +func (u *introspectionResponse) SetSubject(sub string) { + u.Subject = sub +} + +func (u *introspectionResponse) SetName(name string) { + u.Name = name +} + +func (u *introspectionResponse) SetGivenName(name string) { + u.GivenName = name +} + +func (u *introspectionResponse) SetFamilyName(name string) { + u.FamilyName = name +} + +func (u *introspectionResponse) SetMiddleName(name string) { + u.MiddleName = name +} + +func (u *introspectionResponse) SetNickname(name string) { + u.Nickname = name +} + +func (u *introspectionResponse) SetUpdatedAt(date time.Time) { + u.UpdatedAt = Time(date) +} + +func (u *introspectionResponse) SetProfile(profile string) { + u.Profile = profile +} + +func (u *introspectionResponse) SetPicture(picture string) { + u.Picture = picture +} + +func (u *introspectionResponse) SetWebsite(website string) { + u.Website = website +} + +func (u *introspectionResponse) SetGender(gender Gender) { + u.Gender = gender +} + +func (u *introspectionResponse) SetBirthdate(birthdate string) { + u.Birthdate = birthdate +} + +func (u *introspectionResponse) SetZoneinfo(zoneInfo string) { + u.Zoneinfo = zoneInfo +} + +func (u *introspectionResponse) SetLocale(locale language.Tag) { + u.Locale = locale +} + +func (u *introspectionResponse) SetPreferredUsername(name string) { + u.PreferredUsername = name +} + +func (u *introspectionResponse) SetEmail(email string, verified bool) { + u.Email = email + u.EmailVerified = verified +} + +func (u *introspectionResponse) SetPhone(phone string, verified bool) { + u.PhoneNumber = phone + u.PhoneNumberVerified = verified +} + +func (u *introspectionResponse) SetAddress(address UserInfoAddress) { + u.Address = address +} + +func (u *introspectionResponse) AppendClaims(key string, value interface{}) { + if u.claims == nil { + u.claims = make(map[string]interface{}) + } + u.claims[key] = value +} + +func (i *introspectionResponse) MarshalJSON() ([]byte, error) { + type Alias introspectionResponse + a := &struct { + *Alias + Locale interface{} `json:"locale,omitempty"` + UpdatedAt int64 `json:"updated_at,omitempty"` + Username string `json:"username,omitempty"` + }{ + Alias: (*Alias)(i), + } + if !i.Locale.IsRoot() { + a.Locale = i.Locale + } + if !time.Time(i.UpdatedAt).IsZero() { + a.UpdatedAt = time.Time(i.UpdatedAt).Unix() + } + a.Username = i.PreferredUsername + + b, err := json.Marshal(a) + if err != nil { + return nil, err + } + + if len(i.claims) == 0 { + return b, nil + } + + err = json.Unmarshal(b, &i.claims) + if err != nil { + return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims) + } + + return json.Marshal(i.claims) +} + +func (i *introspectionResponse) UnmarshalJSON(data []byte) error { + type Alias introspectionResponse + a := &struct { + *Alias + UpdatedAt int64 `json:"update_at,omitempty"` + }{ + Alias: (*Alias)(i), + } + if err := json.Unmarshal(data, &a); err != nil { + return err + } + + i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC()) + + if err := json.Unmarshal(data, &i.claims); err != nil { + return err + } + + return nil +} diff --git a/pkg/oidc/jwt_profile.go b/pkg/oidc/jwt_profile.go new file mode 100644 index 0000000..6969783 --- /dev/null +++ b/pkg/oidc/jwt_profile.go @@ -0,0 +1,18 @@ +package oidc + +type JWTProfileGrantRequest struct { + Assertion string `schema:"assertion"` + Scope Scopes `schema:"scope"` + GrantType GrantType `schema:"grant_type"` +} + +//NewJWTProfileGrantRequest creates an oauth2 `JSON Web Token (JWT) Profile` Grant +//`urn:ietf:params:oauth:grant-type:jwt-bearer` +//sending a self-signed jwt as assertion +func NewJWTProfileGrantRequest(assertion string, scopes ...string) *JWTProfileGrantRequest { + return &JWTProfileGrantRequest{ + GrantType: GrantTypeBearer, + Assertion: assertion, + Scope: scopes, + } +} diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index ff8f33e..068e8e6 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -1,7 +1,10 @@ package oidc import ( + "crypto/rsa" + "crypto/x509" "encoding/json" + "encoding/pem" "io/ioutil" "time" @@ -14,6 +17,8 @@ import ( const ( //BearerToken defines the token_type `Bearer`, which is returned in a successful token response BearerToken = "Bearer" + + PrefixBearer = BearerToken + " " ) type Tokens struct { @@ -397,7 +402,7 @@ type AccessTokenResponse struct { type JWTProfileAssertion struct { PrivateKeyID string `json:"-"` PrivateKey []byte `json:"-"` - Issuer string `json:"issuer"` + Issuer string `json:"iss"` Subject string `json:"sub"` Audience Audience `json:"aud"` Expiration Time `json:"exp"` @@ -412,6 +417,19 @@ func NewJWTProfileAssertionFromKeyJSON(filename string, audience []string) (*JWT return NewJWTProfileAssertionFromFileData(data, audience) } +func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string) (string, error) { + keyData := new(struct { + KeyID string `json:"keyId"` + Key string `json:"key"` + UserID string `json:"userId"` + }) + err := json.Unmarshal(data, keyData) + if err != nil { + return "", err + } + return GenerateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key))) +} + func NewJWTProfileAssertionFromFileData(data []byte, audience []string) (*JWTProfileAssertion, error) { keyData := new(struct { KeyID string `json:"keyId"` @@ -454,3 +472,46 @@ func AppendClientIDToAudience(clientID string, audience []string) []string { } return append(audience, clientID) } + +func GenerateJWTProfileToken(assertion *JWTProfileAssertion) (string, error) { + privateKey, err := bytesToPrivateKey(assertion.PrivateKey) + if err != nil { + return "", err + } + key := jose.SigningKey{ + Algorithm: jose.RS256, + Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID}, + } + signer, err := jose.NewSigner(key, &jose.SignerOptions{}) + if err != nil { + return "", err + } + + marshalledAssertion, err := json.Marshal(assertion) + if err != nil { + return "", err + } + signedAssertion, err := signer.Sign(marshalledAssertion) + if err != nil { + return "", err + } + return signedAssertion.CompactSerialize() +} + +func bytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(priv) + enc := x509.IsEncryptedPEMBlock(block) + b := block.Bytes + var err error + if enc { + b, err = x509.DecryptPEMBlock(block, nil) + if err != nil { + return nil, err + } + } + key, err := x509.ParsePKCS1PrivateKey(b) + if err != nil { + return nil, err + } + return key, nil +} diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index 1312b18..0c5b70b 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -15,6 +15,10 @@ const ( //GrantTypeTokenExchange defines the grant_type `urn:ietf:params:oauth:grant-type:token-exchange` used for the OAuth Token Exchange Grant GrantTypeTokenExchange GrantType = "urn:ietf:params:oauth:grant-type:token-exchange" + + //ClientAssertionTypeJWTAssertion defines the client_assertion_type `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` + //used for the OAuth JWT Profile Client Authentication + ClientAssertionTypeJWTAssertion = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" ) type GrantType string @@ -27,11 +31,13 @@ type TokenRequest interface { type TokenRequestType GrantType type AccessTokenRequest struct { - Code string `schema:"code"` - RedirectURI string `schema:"redirect_uri"` - ClientID string `schema:"client_id"` - ClientSecret string `schema:"client_secret"` - CodeVerifier string `schema:"code_verifier"` + Code string `schema:"code"` + RedirectURI string `schema:"redirect_uri"` + ClientID string `schema:"client_id"` + ClientSecret string `schema:"client_secret"` + CodeVerifier string `schema:"code_verifier"` + ClientAssertion string `schema:"client_assertion"` + ClientAssertionType string `schema:"client_assertion_type"` } func (a *AccessTokenRequest) GrantType() GrantType { diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index 86e5d06..5525923 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -73,6 +73,19 @@ func (s *Scopes) MarshalText() ([]byte, error) { return []byte(s.Encode()), nil } +func (s *Scopes) MarshalJSON() ([]byte, error) { + return json.Marshal((*s).Encode()) +} + +func (s *Scopes) UnmarshalJSON(data []byte) error { + var str string + if err := json.Unmarshal(data, &str); err != nil { + return err + } + *s = strings.Split(str, " ") + return nil +} + type Time time.Time func (t *Time) UnmarshalJSON(data []byte) error { diff --git a/pkg/oidc/userinfo.go b/pkg/oidc/userinfo.go index 3c77b7b..6bc0016 100644 --- a/pkg/oidc/userinfo.go +++ b/pkg/oidc/userinfo.go @@ -6,8 +6,6 @@ import ( "time" "golang.org/x/text/language" - - "github.com/caos/oidc/pkg/utils" ) type UserInfo interface { @@ -351,11 +349,12 @@ func (i *userinfo) MarshalJSON() ([]byte, error) { return b, nil } - claims, err := json.Marshal(i.claims) + err = json.Unmarshal(b, &i.claims) if err != nil { return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims) } - return utils.ConcatenateJSON(b, claims) + + return json.Marshal(i.claims) } func (i *userinfo) UnmarshalJSON(data []byte) error { @@ -372,6 +371,10 @@ func (i *userinfo) UnmarshalJSON(data []byte) error { i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC()) + if err := json.Unmarshal(data, &i.claims); err != nil { + return err + } + return nil } diff --git a/pkg/op/authrequest.go b/pkg/op/authrequest.go index 9e320f8..3a79b9b 100644 --- a/pkg/op/authrequest.go +++ b/pkg/op/authrequest.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "strings" + "time" "github.com/gorilla/mux" @@ -12,6 +13,23 @@ import ( "github.com/caos/oidc/pkg/utils" ) +type AuthRequest interface { + GetID() string + GetACR() string + GetAMR() []string + GetAudience() []string + GetAuthTime() time.Time + GetClientID() string + GetCodeChallenge() *oidc.CodeChallenge + GetNonce() string + GetRedirectURI() string + GetResponseType() oidc.ResponseType + GetScopes() []string + GetState() string + GetSubject() string + Done() bool +} + type Authorizer interface { Storage() Storage Decoder() utils.Decoder diff --git a/pkg/op/client.go b/pkg/op/client.go index 6d0891c..79715b0 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -28,7 +28,7 @@ type Client interface { RedirectURIs() []string PostLogoutRedirectURIs() []string ApplicationType() ApplicationType - AuthMethod() AuthMethod + AuthMethod() oidc.AuthMethod ResponseTypes() []oidc.ResponseType LoginURL(string) string AccessTokenType() AccessTokenType diff --git a/pkg/op/config.go b/pkg/op/config.go index a2b831e..7cb522a 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -13,12 +13,14 @@ type Configuration interface { Issuer() string AuthorizationEndpoint() Endpoint TokenEndpoint() Endpoint + IntrospectionEndpoint() Endpoint UserinfoEndpoint() Endpoint EndSessionEndpoint() Endpoint KeysEndpoint() Endpoint AuthMethodPostSupported() bool CodeMethodS256Supported() bool + AuthMethodPrivateKeyJWTSupported() bool GrantTypeTokenExchangeSupported() bool GrantTypeJWTAuthorizationSupported() bool } diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 4bc1272..d8ef7c3 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -3,6 +3,8 @@ package op import ( "net/http" + "golang.org/x/text/language" + "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/utils" ) @@ -19,22 +21,23 @@ func Discover(w http.ResponseWriter, config *oidc.DiscoveryConfiguration) { func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfiguration { return &oidc.DiscoveryConfiguration{ - Issuer: c.Issuer(), - AuthorizationEndpoint: c.AuthorizationEndpoint().Absolute(c.Issuer()), - TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()), - // IntrospectionEndpoint: c.Intro().Absolute(c.Issuer()), - UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()), - EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), - // CheckSessionIframe: c.TokenEndpoint().Absolute(c.Issuer())(c.CheckSessionIframe), - JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), - ScopesSupported: Scopes(c), - ResponseTypesSupported: ResponseTypes(c), - GrantTypesSupported: GrantTypes(c), - ClaimsSupported: SupportedClaims(c), - IDTokenSigningAlgValuesSupported: SigAlgorithms(s), - SubjectTypesSupported: SubjectTypes(c), - TokenEndpointAuthMethodsSupported: AuthMethods(c), - CodeChallengeMethodsSupported: CodeChallengeMethods(c), + Issuer: c.Issuer(), + AuthorizationEndpoint: c.AuthorizationEndpoint().Absolute(c.Issuer()), + TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()), + IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()), + UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()), + EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), + JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), + ScopesSupported: Scopes(c), + ResponseTypesSupported: ResponseTypes(c), + GrantTypesSupported: GrantTypes(c), + SubjectTypesSupported: SubjectTypes(c), + IDTokenSigningAlgValuesSupported: SigAlgorithms(s), + TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(c), + IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(c), + ClaimsSupported: SupportedClaims(c), + CodeChallengeMethodsSupported: CodeChallengeMethods(c), + UILocalesSupported: UILocales(c), } } @@ -58,15 +61,16 @@ func ResponseTypes(c Configuration) []string { } //TODO: ok for now, check later if dynamic needed } -func GrantTypes(c Configuration) []string { - grantTypes := []string{ - string(oidc.GrantTypeCode), +func GrantTypes(c Configuration) []oidc.GrantType { + grantTypes := []oidc.GrantType{ + oidc.GrantTypeCode, + oidc.GrantTypeImplicit, } if c.GrantTypeTokenExchangeSupported() { - grantTypes = append(grantTypes, string(oidc.GrantTypeTokenExchange)) + grantTypes = append(grantTypes, oidc.GrantTypeTokenExchange) } if c.GrantTypeJWTAuthorizationSupported() { - grantTypes = append(grantTypes, string(oidc.GrantTypeBearer)) + grantTypes = append(grantTypes, oidc.GrantTypeBearer) } return grantTypes } @@ -108,20 +112,41 @@ func SubjectTypes(c Configuration) []string { return []string{"public"} //TODO: config } -func AuthMethods(c Configuration) []string { - authMethods := []string{ - string(AuthMethodBasic), +func AuthMethodsTokenEndpoint(c Configuration) []oidc.AuthMethod { + authMethods := []oidc.AuthMethod{ + oidc.AuthMethodNone, + oidc.AuthMethodBasic, } if c.AuthMethodPostSupported() { - authMethods = append(authMethods, string(AuthMethodPost)) + authMethods = append(authMethods, oidc.AuthMethodPost) + } + if c.AuthMethodPrivateKeyJWTSupported() { + authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT) } return authMethods } -func CodeChallengeMethods(c Configuration) []string { - codeMethods := make([]string, 0, 1) +func AuthMethodsIntrospectionEndpoint(c Configuration) []oidc.AuthMethod { + authMethods := []oidc.AuthMethod{ + oidc.AuthMethodBasic, + } + if c.AuthMethodPrivateKeyJWTSupported() { + authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT) + } + return authMethods +} + +func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod { + codeMethods := make([]oidc.CodeChallengeMethod, 0, 1) if c.CodeMethodS256Supported() { - codeMethods = append(codeMethods, CodeMethodS256) + codeMethods = append(codeMethods, oidc.CodeChallengeMethodS256) } return codeMethods } + +func UILocales(c Configuration) []language.Tag { + return []language.Tag{ + language.English, + language.German, + } +} diff --git a/pkg/op/discovery_test.go b/pkg/op/discovery_test.go index c14fac4..4d97a01 100644 --- a/pkg/op/discovery_test.go +++ b/pkg/op/discovery_test.go @@ -37,7 +37,7 @@ func TestDiscover(t *testing.T) { op.Discover(tt.args.w, tt.args.config) rec := tt.args.w.(*httptest.ResponseRecorder) require.Equal(t, http.StatusOK, rec.Code) - require.Equal(t, `{"issuer":"https://issuer.com"}`, rec.Body.String()) + require.Equal(t, `{"issuer":"https://issuer.com","request_uri_parameter_supported":false}`, rec.Body.String()) }) } } @@ -199,36 +199,49 @@ func Test_SubjectTypes(t *testing.T) { } } -func Test_AuthMethods(t *testing.T) { - m := mock.NewMockConfiguration(gomock.NewController(t)) +func Test_AuthMethodsTokenEndpoint(t *testing.T) { type args struct { c op.Configuration } tests := []struct { name string args args - want []string + want []oidc.AuthMethod }{ { - "imlicit basic", + "none and basic", args{func() op.Configuration { + m := mock.NewMockConfiguration(gomock.NewController(t)) m.EXPECT().AuthMethodPostSupported().Return(false) + m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false) return m }()}, - []string{string(op.AuthMethodBasic)}, + []oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic}, }, { - "basic and post", + "none, basic and post", args{func() op.Configuration { + m := mock.NewMockConfiguration(gomock.NewController(t)) m.EXPECT().AuthMethodPostSupported().Return(true) + m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false) return m }()}, - []string{string(op.AuthMethodBasic), string(op.AuthMethodPost)}, + []oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost}, + }, + { + "none, basic, post and private_key_jwt", + args{func() op.Configuration { + m := mock.NewMockConfiguration(gomock.NewController(t)) + m.EXPECT().AuthMethodPostSupported().Return(true) + m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true) + return m + }()}, + []oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost, oidc.AuthMethodPrivateKeyJWT}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := op.AuthMethods(tt.args.c); !reflect.DeepEqual(got, tt.want) { + if got := op.AuthMethodsTokenEndpoint(tt.args.c); !reflect.DeepEqual(got, tt.want) { t.Errorf("authMethods() = %v, want %v", got, tt.want) } }) diff --git a/pkg/op/mock/client.mock.go b/pkg/op/mock/client.mock.go index 1a15624..9d5fe41 100644 --- a/pkg/op/mock/client.mock.go +++ b/pkg/op/mock/client.mock.go @@ -64,10 +64,10 @@ func (mr *MockClientMockRecorder) ApplicationType() *gomock.Call { } // AuthMethod mocks base method -func (m *MockClient) AuthMethod() op.AuthMethod { +func (m *MockClient) AuthMethod() oidc.AuthMethod { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AuthMethod") - ret0, _ := ret[0].(op.AuthMethod) + ret0, _ := ret[0].(oidc.AuthMethod) return ret0 } diff --git a/pkg/op/mock/configuration.mock.go b/pkg/op/mock/configuration.mock.go index ece747c..4f83f35 100644 --- a/pkg/op/mock/configuration.mock.go +++ b/pkg/op/mock/configuration.mock.go @@ -47,6 +47,20 @@ func (mr *MockConfigurationMockRecorder) AuthMethodPostSupported() *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthMethodPostSupported", reflect.TypeOf((*MockConfiguration)(nil).AuthMethodPostSupported)) } +// AuthMethodPrivateKeyJWTSupported mocks base method +func (m *MockConfiguration) AuthMethodPrivateKeyJWTSupported() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AuthMethodPrivateKeyJWTSupported") + ret0, _ := ret[0].(bool) + return ret0 +} + +// AuthMethodPrivateKeyJWTSupported indicates an expected call of AuthMethodPrivateKeyJWTSupported +func (mr *MockConfigurationMockRecorder) AuthMethodPrivateKeyJWTSupported() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthMethodPrivateKeyJWTSupported", reflect.TypeOf((*MockConfiguration)(nil).AuthMethodPrivateKeyJWTSupported)) +} + // AuthorizationEndpoint mocks base method func (m *MockConfiguration) AuthorizationEndpoint() op.Endpoint { m.ctrl.T.Helper() @@ -117,6 +131,20 @@ func (mr *MockConfigurationMockRecorder) GrantTypeTokenExchangeSupported() *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeTokenExchangeSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeTokenExchangeSupported)) } +// IntrospectionEndpoint mocks base method +func (m *MockConfiguration) IntrospectionEndpoint() op.Endpoint { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IntrospectionEndpoint") + ret0, _ := ret[0].(op.Endpoint) + return ret0 +} + +// IntrospectionEndpoint indicates an expected call of IntrospectionEndpoint +func (mr *MockConfigurationMockRecorder) IntrospectionEndpoint() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntrospectionEndpoint", reflect.TypeOf((*MockConfiguration)(nil).IntrospectionEndpoint)) +} + // Issuer mocks base method func (m *MockConfiguration) Issuer() string { m.ctrl.T.Helper() diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index 9e4963a..e589413 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -198,36 +198,6 @@ func (mr *MockStorageMockRecorder) GetSigningKey(arg0, arg1, arg2, arg3 interfac return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSigningKey", reflect.TypeOf((*MockStorage)(nil).GetSigningKey), arg0, arg1, arg2, arg3) } -// GetUserinfoFromScopes mocks base method -func (m *MockStorage) GetUserinfoFromScopes(arg0 context.Context, arg1, arg2 string, arg3 []string) (oidc.UserInfo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserinfoFromScopes", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(oidc.UserInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserinfoFromScopes indicates an expected call of GetUserinfoFromScopes -func (mr *MockStorageMockRecorder) GetUserinfoFromScopes(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserinfoFromScopes", reflect.TypeOf((*MockStorage)(nil).GetUserinfoFromScopes), arg0, arg1, arg2, arg3) -} - -// GetUserinfoFromToken mocks base method -func (m *MockStorage) GetUserinfoFromToken(arg0 context.Context, arg1, arg2, arg3 string) (oidc.UserInfo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserinfoFromToken", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(oidc.UserInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserinfoFromToken indicates an expected call of GetUserinfoFromToken -func (mr *MockStorageMockRecorder) GetUserinfoFromToken(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserinfoFromToken", reflect.TypeOf((*MockStorage)(nil).GetUserinfoFromToken), arg0, arg1, arg2, arg3) -} - // Health mocks base method func (m *MockStorage) Health(arg0 context.Context) error { m.ctrl.T.Helper() @@ -270,6 +240,48 @@ func (mr *MockStorageMockRecorder) SaveNewKeyPair(arg0 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveNewKeyPair", reflect.TypeOf((*MockStorage)(nil).SaveNewKeyPair), arg0) } +// SetIntrospectionFromToken mocks base method +func (m *MockStorage) SetIntrospectionFromToken(arg0 context.Context, arg1 oidc.IntrospectionResponse, arg2, arg3, arg4 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetIntrospectionFromToken", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetIntrospectionFromToken indicates an expected call of SetIntrospectionFromToken +func (mr *MockStorageMockRecorder) SetIntrospectionFromToken(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIntrospectionFromToken", reflect.TypeOf((*MockStorage)(nil).SetIntrospectionFromToken), arg0, arg1, arg2, arg3, arg4) +} + +// SetUserinfoFromScopes mocks base method +func (m *MockStorage) SetUserinfoFromScopes(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3 string, arg4 []string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetUserinfoFromScopes", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetUserinfoFromScopes indicates an expected call of SetUserinfoFromScopes +func (mr *MockStorageMockRecorder) SetUserinfoFromScopes(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserinfoFromScopes", reflect.TypeOf((*MockStorage)(nil).SetUserinfoFromScopes), arg0, arg1, arg2, arg3, arg4) +} + +// SetUserinfoFromToken mocks base method +func (m *MockStorage) SetUserinfoFromToken(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3, arg4 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetUserinfoFromToken", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetUserinfoFromToken indicates an expected call of SetUserinfoFromToken +func (mr *MockStorageMockRecorder) SetUserinfoFromToken(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserinfoFromToken", reflect.TypeOf((*MockStorage)(nil).SetUserinfoFromToken), arg0, arg1, arg2, arg3, arg4) +} + // TerminateSession mocks base method func (m *MockStorage) TerminateSession(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() @@ -283,3 +295,18 @@ func (mr *MockStorageMockRecorder) TerminateSession(arg0, arg1, arg2 interface{} mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TerminateSession", reflect.TypeOf((*MockStorage)(nil).TerminateSession), arg0, arg1, arg2) } + +// ValidateJWTProfileScopes mocks base method +func (m *MockStorage) ValidateJWTProfileScopes(arg0 context.Context, arg1 string, arg2 oidc.Scopes) (oidc.Scopes, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateJWTProfileScopes", arg0, arg1, arg2) + ret0, _ := ret[0].(oidc.Scopes) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ValidateJWTProfileScopes indicates an expected call of ValidateJWTProfileScopes +func (mr *MockStorageMockRecorder) ValidateJWTProfileScopes(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateJWTProfileScopes", reflect.TypeOf((*MockStorage)(nil).ValidateJWTProfileScopes), arg0, arg1, arg2) +} diff --git a/pkg/op/mock/storage.mock.impl.go b/pkg/op/mock/storage.mock.impl.go index 29d0d15..2788c39 100644 --- a/pkg/op/mock/storage.mock.impl.go +++ b/pkg/op/mock/storage.mock.impl.go @@ -65,23 +65,23 @@ func ExpectValidClientID(s op.Storage) { mockS.EXPECT().GetClientByClientID(gomock.Any(), gomock.Any()).DoAndReturn( func(_ context.Context, id string) (op.Client, error) { var appType op.ApplicationType - var authMethod op.AuthMethod + var authMethod oidc.AuthMethod var accessTokenType op.AccessTokenType var responseTypes []oidc.ResponseType switch id { case "web_client": appType = op.ApplicationTypeWeb - authMethod = op.AuthMethodBasic + authMethod = oidc.AuthMethodBasic accessTokenType = op.AccessTokenTypeBearer responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode} case "native_client": appType = op.ApplicationTypeNative - authMethod = op.AuthMethodNone + authMethod = oidc.AuthMethodNone accessTokenType = op.AccessTokenTypeBearer responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode} case "useragent_client": appType = op.ApplicationTypeUserAgent - authMethod = op.AuthMethodBasic + authMethod = oidc.AuthMethodBasic accessTokenType = op.AccessTokenTypeJWT responseTypes = []oidc.ResponseType{oidc.ResponseTypeIDToken} } @@ -119,7 +119,7 @@ func ExpectSigningKey(s op.Storage) { type ConfClient struct { id string appType op.ApplicationType - authMethod op.AuthMethod + authMethod oidc.AuthMethod accessTokenType op.AccessTokenType responseTypes []oidc.ResponseType devMode bool @@ -145,7 +145,7 @@ func (c *ConfClient) ApplicationType() op.ApplicationType { return c.appType } -func (c *ConfClient) AuthMethod() op.AuthMethod { +func (c *ConfClient) AuthMethod() oidc.AuthMethod { return c.authMethod } diff --git a/pkg/op/op.go b/pkg/op/op.go index d16848e..26445c5 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -17,26 +17,20 @@ import ( ) const ( - healthzEndpoint = "/healthz" + healthEndpoint = "/healthz" readinessEndpoint = "/ready" defaultAuthorizationEndpoint = "authorize" - defaulTokenEndpoint = "oauth/token" - defaultIntrospectEndpoint = "introspect" + defaultTokenEndpoint = "oauth/token" + defaultIntrospectEndpoint = "oauth/introspect" defaultUserinfoEndpoint = "userinfo" defaultEndSessionEndpoint = "end_session" defaultKeysEndpoint = "keys" - - AuthMethodBasic AuthMethod = "client_secret_basic" - AuthMethodPost AuthMethod = "client_secret_post" - AuthMethodNone AuthMethod = "none" - - CodeMethodS256 = "S256" ) var ( DefaultEndpoints = &endpoints{ Authorization: NewEndpoint(defaultAuthorizationEndpoint), - Token: NewEndpoint(defaulTokenEndpoint), + Token: NewEndpoint(defaultTokenEndpoint), Introspection: NewEndpoint(defaultIntrospectEndpoint), Userinfo: NewEndpoint(defaultUserinfoEndpoint), EndSession: NewEndpoint(defaultEndSessionEndpoint), @@ -72,12 +66,13 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router handlers.AllowedHeaders([]string{"authorization", "content-type"}), handlers.AllowedOriginValidator(allowAllOrigins), )) - router.HandleFunc(healthzEndpoint, healthzHandler) + router.HandleFunc(healthEndpoint, healthHandler) router.HandleFunc(readinessEndpoint, readyHandler(o.Probes())) router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Signer())) router.Handle(o.AuthorizationEndpoint().Relative(), intercept(authorizeHandler(o))) router.NewRoute().Path(o.AuthorizationEndpoint().Relative()+"/callback").Queries("id", "{id}").Handler(intercept(authorizeCallbackHandler(o))) router.Handle(o.TokenEndpoint().Relative(), intercept(tokenHandler(o))) + router.HandleFunc(o.IntrospectionEndpoint().Relative(), introspectionHandler(o)) router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o)) router.Handle(o.EndSessionEndpoint().Relative(), intercept(endSessionHandler(o))) router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o)) @@ -89,6 +84,7 @@ type Config struct { CryptoKey [32]byte DefaultLogoutRedirectURI string CodeMethodS256 bool + AuthMethodPrivateKeyJWT bool } type endpoints struct { @@ -166,6 +162,10 @@ func (o *openidProvider) TokenEndpoint() Endpoint { return o.endpoints.Token } +func (o *openidProvider) IntrospectionEndpoint() Endpoint { + return o.endpoints.Introspection +} + func (o *openidProvider) UserinfoEndpoint() Endpoint { return o.endpoints.Userinfo } @@ -186,6 +186,10 @@ func (o *openidProvider) CodeMethodS256Supported() bool { return o.config.CodeMethodS256 } +func (o *openidProvider) AuthMethodPrivateKeyJWTSupported() bool { + return o.config.AuthMethodPrivateKeyJWT +} + func (o *openidProvider) GrantTypeTokenExchangeSupported() bool { return false } @@ -332,6 +336,16 @@ func WithCustomTokenEndpoint(endpoint Endpoint) Option { } } +func WithCustomIntrospectionEndpoint(endpoint Endpoint) Option { + return func(o *openidProvider) error { + if err := endpoint.Validate(); err != nil { + return err + } + o.endpoints.Introspection = endpoint + return nil + } +} + func WithCustomUserinfoEndpoint(endpoint Endpoint) Option { return func(o *openidProvider) error { if err := endpoint.Validate(); err != nil { diff --git a/pkg/op/probes.go b/pkg/op/probes.go index 7dc00a9..c6bb748 100644 --- a/pkg/op/probes.go +++ b/pkg/op/probes.go @@ -10,7 +10,7 @@ import ( type ProbesFn func(context.Context) error -func healthzHandler(w http.ResponseWriter, r *http.Request) { +func healthHandler(w http.ResponseWriter, r *http.Request) { ok(w) } diff --git a/pkg/op/storage.go b/pkg/op/storage.go index eba5003..33ed6ce 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -28,10 +28,12 @@ type AuthStorage interface { type OPStorage interface { GetClientByClientID(ctx context.Context, clientID string) (Client, error) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error - GetUserinfoFromScopes(ctx context.Context, userID, clientID string, scopes []string) (oidc.UserInfo, error) - GetUserinfoFromToken(ctx context.Context, tokenID, subject, origin string) (oidc.UserInfo, error) + SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error + SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error + SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, clientID string) error GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) + ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) } type Storage interface { @@ -44,23 +46,6 @@ type StorageNotFoundError interface { IsNotFound() } -type AuthRequest interface { - GetID() string - GetACR() string - GetAMR() []string - GetAudience() []string - GetAuthTime() time.Time - GetClientID() string - GetCodeChallenge() *oidc.CodeChallenge - GetNonce() string - GetRedirectURI() string - GetResponseType() oidc.ResponseType - GetScopes() []string - GetState() string - GetSubject() string - Done() bool -} - type EndSessionRequest struct { UserID string Client Client diff --git a/pkg/op/token.go b/pkg/op/token.go index 5331d44..334bec9 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -114,7 +114,8 @@ func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, vali } } if len(scopes) > 0 { - userInfo, err := storage.GetUserinfoFromScopes(ctx, authReq.GetSubject(), authReq.GetClientID(), scopes) + userInfo := oidc.NewUserInfo() + err := storage.SetUserinfoFromScopes(ctx, userInfo, authReq.GetSubject(), authReq.GetClientID(), scopes) if err != nil { return "", err } diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go new file mode 100644 index 0000000..30d2544 --- /dev/null +++ b/pkg/op/token_intospection.go @@ -0,0 +1,77 @@ +package op + +import ( + "errors" + "net/http" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +type Introspector interface { + Decoder() utils.Decoder + Crypto() Crypto + Storage() Storage + AccessTokenVerifier() AccessTokenVerifier +} + +type IntrospectorJWTProfile interface { + Introspector + JWTProfileVerifier() JWTProfileVerifier +} + +func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + Introspect(w, r, introspector) + } +} + +func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspector) { + response := oidc.NewIntrospectionResponse() + token, clientID, err := ParseTokenIntrospectionRequest(r, introspector) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, token) + if !ok { + utils.MarshalJSON(w, response) + return + } + err = introspector.Storage().SetIntrospectionFromToken(r.Context(), response, tokenID, subject, clientID) + if err != nil { + utils.MarshalJSON(w, response) + return + } + response.SetActive(true) + utils.MarshalJSON(w, response) +} + +func ParseTokenIntrospectionRequest(r *http.Request, introspector Introspector) (token, clientID string, err error) { + err = r.ParseForm() + if err != nil { + return "", "", errors.New("unable to parse request") + } + req := new(struct { + oidc.IntrospectionRequest + oidc.ClientAssertionParams + }) + err = introspector.Decoder().Decode(req, r.Form) + if err != nil { + return "", "", errors.New("unable to parse request") + } + if introspectorJWTProfile, ok := introspector.(IntrospectorJWTProfile); ok && req.ClientAssertion != "" { + profile, err := VerifyJWTAssertion(r.Context(), req.ClientAssertion, introspectorJWTProfile.JWTProfileVerifier()) + if err == nil { + return req.Token, profile.Issuer, nil + } + } + clientID, clientSecret, ok := r.BasicAuth() + if ok { + if err := introspector.Storage().AuthorizeClientIDSecret(r.Context(), clientID, clientSecret); err != nil { + return "", "", err + } + return req.Token, clientID, nil + } + return "", "", errors.New("invalid authorization") +} diff --git a/pkg/op/tokenrequest.go b/pkg/op/tokenrequest.go index c3860ff..b51d2c8 100644 --- a/pkg/op/tokenrequest.go +++ b/pkg/op/tokenrequest.go @@ -7,7 +7,6 @@ import ( "net/url" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" "github.com/caos/oidc/pkg/utils" ) @@ -18,6 +17,7 @@ type Exchanger interface { Signer() Signer Crypto() Crypto AuthMethodPostSupported() bool + AuthMethodPrivateKeyJWTSupported() bool GrantTypeTokenExchangeSupported() bool GrantTypeJWTAuthorizationSupported() bool } @@ -112,18 +112,33 @@ func ValidateAccessTokenRequest(ctx context.Context, tokenReq *oidc.AccessTokenR } func AuthorizeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (AuthRequest, Client, error) { + if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion { + jwtExchanger, ok := exchanger.(JWTAuthorizationGrantExchanger) + if !ok || !exchanger.AuthMethodPrivateKeyJWTSupported() { + return nil, nil, errors.New("auth_method private_key_jwt not supported") + } + return AuthorizePrivateJWTKey(ctx, tokenReq, jwtExchanger) + } client, err := exchanger.Storage().GetClientByClientID(ctx, tokenReq.ClientID) if err != nil { return nil, nil, err } - if client.AuthMethod() == AuthMethodNone { + if client.AuthMethod() == oidc.AuthMethodPrivateKeyJWT { + return nil, nil, errors.New("invalid_grant") + } + if client.AuthMethod() == oidc.AuthMethodNone { authReq, err := AuthorizeCodeChallenge(ctx, tokenReq, exchanger) return authReq, client, err } - if client.AuthMethod() == AuthMethodPost && !exchanger.AuthMethodPostSupported() { + if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() { return nil, nil, errors.New("auth_method post not supported") } - err = AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, exchanger.Storage()) + authReq, err := AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, tokenReq.Code, exchanger.Storage()) + return authReq, client, err +} + +func AuthorizePrivateJWTKey(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger JWTAuthorizationGrantExchanger) (AuthRequest, Client, error) { + jwtReq, err := VerifyJWTAssertion(ctx, tokenReq.ClientAssertion, exchanger.JWTProfileVerifier()) if err != nil { return nil, nil, err } @@ -131,11 +146,26 @@ func AuthorizeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exc if err != nil { return nil, nil, ErrInvalidRequest("invalid code") } + client, err := exchanger.Storage().GetClientByClientID(ctx, jwtReq.Issuer) + if err != nil { + return nil, nil, err + } + if client.AuthMethod() != oidc.AuthMethodPrivateKeyJWT { + return nil, nil, ErrInvalidRequest("invalid_client") + } return authReq, client, nil } -func AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string, storage OPStorage) error { - return storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret) +func AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret, code string, storage Storage) (AuthRequest, error) { + err := storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret) + if err != nil { + return nil, err + } + authReq, err := storage.AuthRequestByCode(ctx, code) + if err != nil { + return nil, ErrInvalidRequest("invalid code") + } + return authReq, nil } func AuthorizeCodeChallenge(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (AuthRequest, error) { @@ -158,12 +188,17 @@ func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizati RequestError(w, r, err) } - tokenRequest, err := VerifyJWTAssertion(r.Context(), profileRequest, exchanger.JWTProfileVerifier()) + tokenRequest, err := VerifyJWTAssertion(r.Context(), profileRequest.Assertion, exchanger.JWTProfileVerifier()) if err != nil { RequestError(w, r, err) return } + tokenRequest.Scopes, err = exchanger.Storage().ValidateJWTProfileScopes(r.Context(), tokenRequest.Issuer, profileRequest.Scope) + if err != nil { + RequestError(w, r, err) + return + } resp, err := CreateJWTTokenResponse(r.Context(), tokenRequest, exchanger) if err != nil { RequestError(w, r, err) @@ -172,12 +207,12 @@ func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizati utils.MarshalJSON(w, resp) } -func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (*tokenexchange.JWTProfileRequest, error) { +func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (*oidc.JWTProfileGrantRequest, error) { err := r.ParseForm() if err != nil { return nil, ErrInvalidRequest("error parsing form") } - tokenReq := new(tokenexchange.JWTProfileRequest) + tokenReq := new(oidc.JWTProfileGrantRequest) err = decoder.Decode(tokenReq, r.Form) if err != nil { return nil, ErrInvalidRequest("error decoding form") diff --git a/pkg/op/userinfo.go b/pkg/op/userinfo.go index 1163598..9abf378 100644 --- a/pkg/op/userinfo.go +++ b/pkg/op/userinfo.go @@ -24,7 +24,7 @@ func userinfoHandler(userinfoProvider UserinfoProvider) func(http.ResponseWriter } func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoProvider) { - accessToken, err := getAccessToken(r, userinfoProvider.Decoder()) + accessToken, err := ParseUserinfoRequest(r, userinfoProvider.Decoder()) if err != nil { http.Error(w, "access token missing", http.StatusUnauthorized) return @@ -34,7 +34,8 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP http.Error(w, "access token invalid", http.StatusUnauthorized) return } - info, err := userinfoProvider.Storage().GetUserinfoFromToken(r.Context(), tokenID, subject, r.Header.Get("origin")) + info := oidc.NewUserInfo() + err = userinfoProvider.Storage().SetUserinfoFromToken(r.Context(), info, tokenID, subject, r.Header.Get("origin")) if err != nil { w.WriteHeader(http.StatusForbidden) utils.MarshalJSON(w, err) @@ -43,16 +44,12 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP utils.MarshalJSON(w, info) } -func getAccessToken(r *http.Request, decoder utils.Decoder) (string, error) { - authHeader := r.Header.Get("authorization") - if authHeader != "" { - parts := strings.Split(authHeader, "Bearer ") - if len(parts) != 2 { - return "", errors.New("invalid auth header") - } - return parts[1], nil +func ParseUserinfoRequest(r *http.Request, decoder utils.Decoder) (string, error) { + accessToken, err := getAccessToken(r) + if err == nil { + return accessToken, nil } - err := r.ParseForm() + err = r.ParseForm() if err != nil { return "", errors.New("unable to parse request") } @@ -64,6 +61,18 @@ func getAccessToken(r *http.Request, decoder utils.Decoder) (string, error) { return req.AccessToken, nil } +func getAccessToken(r *http.Request) (string, error) { + authHeader := r.Header.Get("authorization") + if authHeader == "" { + return "", errors.New("no auth header") + } + parts := strings.Split(authHeader, "Bearer ") + if len(parts) != 2 { + return "", errors.New("invalid auth header") + } + return parts[1], nil +} + func getTokenIDAndSubject(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, bool) { tokenIDSubject, err := userinfoProvider.Crypto().Decrypt(accessToken) if err == nil { diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index 8a31253..03d8264 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -8,7 +8,6 @@ import ( "gopkg.in/square/go-jose.v2" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" ) type JWTProfileVerifier interface { @@ -48,9 +47,9 @@ func (v *jwtProfileVerifier) Offset() time.Duration { return v.offset } -func VerifyJWTAssertion(ctx context.Context, profileRequest *tokenexchange.JWTProfileRequest, v JWTProfileVerifier) (*oidc.JWTTokenRequest, error) { +func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerifier) (*oidc.JWTTokenRequest, error) { request := new(oidc.JWTTokenRequest) - payload, err := oidc.ParseToken(profileRequest.Assertion, request) + payload, err := oidc.ParseToken(assertion, request) if err != nil { return nil, err } @@ -71,12 +70,11 @@ func VerifyJWTAssertion(ctx context.Context, profileRequest *tokenexchange.JWTPr //TODO: implement delegation (openid core / oauth rfc) } - keySet := &jwtProfileKeySet{v.Storage(), request.Subject} + keySet := &jwtProfileKeySet{v.Storage(), request.Issuer} - if err = oidc.CheckSignature(ctx, profileRequest.Assertion, payload, request, nil, keySet); err != nil { + if err = oidc.CheckSignature(ctx, assertion, payload, request, nil, keySet); err != nil { return nil, err } - request.Scopes = profileRequest.Scope return request, nil } diff --git a/pkg/rp/tockenexchange.go b/pkg/rp/tockenexchange.go deleted file mode 100644 index 0fa3725..0000000 --- a/pkg/rp/tockenexchange.go +++ /dev/null @@ -1,100 +0,0 @@ -package rp - -import ( - "context" - "crypto/rsa" - "crypto/x509" - "encoding/json" - "encoding/pem" - - "golang.org/x/oauth2" - "gopkg.in/square/go-jose.v2" - - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" -) - -//TokenExchangeRP extends the `RelayingParty` interface for the *draft* oauth2 `Token Exchange` -type TokenExchangeRP interface { - RelayingParty - - //TokenExchange implement the `Token Exchange Grant` exchanging some token for an other - TokenExchange(context.Context, *tokenexchange.TokenExchangeRequest) (*oauth2.Token, error) -} - -//DelegationTokenExchangeRP extends the `TokenExchangeRP` interface -//for the specific `delegation token` request -type DelegationTokenExchangeRP interface { - TokenExchangeRP - - //DelegationTokenExchange implement the `Token Exchange Grant` - //providing an access token in request for a `delegation` token for a given resource / audience - DelegationTokenExchange(context.Context, string, ...tokenexchange.TokenExchangeOption) (*oauth2.Token, error) -} - -//TokenExchange handles the oauth2 token exchange -func TokenExchange(ctx context.Context, request *tokenexchange.TokenExchangeRequest, rp RelayingParty) (newToken *oauth2.Token, err error) { - return CallTokenEndpoint(request, rp) -} - -//DelegationTokenExchange handles the oauth2 token exchange for a delegation token -func DelegationTokenExchange(ctx context.Context, subjectToken string, rp RelayingParty, reqOpts ...tokenexchange.TokenExchangeOption) (newToken *oauth2.Token, err error) { - return TokenExchange(ctx, DelegationTokenRequest(subjectToken, reqOpts...), rp) -} - -//JWTProfileExchange handles the oauth2 jwt profile exchange -func JWTProfileExchange(ctx context.Context, jwtProfileRequest *tokenexchange.JWTProfileRequest, rp RelayingParty) (*oauth2.Token, error) { - return CallTokenEndpoint(jwtProfileRequest, rp) -} - -//JWTProfileExchange handles the oauth2 jwt profile exchange -func JWTProfileAssertionExchange(ctx context.Context, assertion *oidc.JWTProfileAssertion, scopes oidc.Scopes, rp RelayingParty) (*oauth2.Token, error) { - token, err := GenerateJWTProfileToken(assertion) - if err != nil { - return nil, err - } - return JWTProfileExchange(ctx, tokenexchange.NewJWTProfileRequest(token, scopes...), rp) -} - -func GenerateJWTProfileToken(assertion *oidc.JWTProfileAssertion) (string, error) { - privateKey, err := bytesToPrivateKey(assertion.PrivateKey) - if err != nil { - return "", err - } - key := jose.SigningKey{ - Algorithm: jose.RS256, - Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID}, - } - signer, err := jose.NewSigner(key, &jose.SignerOptions{}) - if err != nil { - return "", err - } - - marshalledAssertion, err := json.Marshal(assertion) - if err != nil { - return "", err - } - signedAssertion, err := signer.Sign(marshalledAssertion) - if err != nil { - return "", err - } - return signedAssertion.CompactSerialize() -} - -func bytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) { - block, _ := pem.Decode(priv) - enc := x509.IsEncryptedPEMBlock(block) - b := block.Bytes - var err error - if enc { - b, err = x509.DecryptPEMBlock(block, nil) - if err != nil { - return nil, err - } - } - key, err := x509.ParsePKCS1PrivateKey(b) - if err != nil { - return nil, err - } - return key, nil -} diff --git a/pkg/utils/key.go b/pkg/utils/key.go new file mode 100644 index 0000000..7965c85 --- /dev/null +++ b/pkg/utils/key.go @@ -0,0 +1,25 @@ +package utils + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" +) + +func BytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(priv) + enc := x509.IsEncryptedPEMBlock(block) + b := block.Bytes + var err error + if enc { + b, err = x509.DecryptPEMBlock(block, nil) + if err != nil { + return nil, err + } + } + key, err := x509.ParsePKCS1PrivateKey(b) + if err != nil { + return nil, err + } + return key, nil +} From 0fabbc33cfd55d4251195caaaa92024520d6dcbc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Feb 2021 10:28:14 +0100 Subject: [PATCH 029/502] chore(deps): bump github.com/sirupsen/logrus from 1.7.0 to 1.8.0 (#85) Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.7.0 to 1.8.0. - [Release notes](https://github.com/sirupsen/logrus/releases) - [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md) - [Commits](https://github.com/sirupsen/logrus/compare/v1.7.0...v1.8.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ad3cdea..aa01202 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/gorilla/schema v1.2.0 github.com/gorilla/securecookie v1.1.1 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect - github.com/sirupsen/logrus v1.7.0 + github.com/sirupsen/logrus v1.8.0 github.com/stretchr/testify v1.7.0 golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 diff --git a/go.sum b/go.sum index edb89be..633efcc 100644 --- a/go.sum +++ b/go.sum @@ -131,6 +131,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g= +github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -139,8 +141,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU= +github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From 01e5b74ba79724e2d3dccbc3d97c592cac39e5a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Feb 2021 10:30:27 +0100 Subject: [PATCH 030/502] chore(deps): bump github.com/golang/mock from 1.4.4 to 1.5.0 (#86) Bumps [github.com/golang/mock](https://github.com/golang/mock) from 1.4.4 to 1.5.0. - [Release notes](https://github.com/golang/mock/releases) - [Changelog](https://github.com/golang/mock/blob/master/.goreleaser.yml) - [Commits](https://github.com/golang/mock/compare/v1.4.4...v1.5.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index aa01202..5dc5249 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.15 require ( github.com/caos/logging v0.0.2 - github.com/golang/mock v1.4.4 + github.com/golang/mock v1.5.0 github.com/google/go-cmp v0.5.2 // indirect github.com/google/go-github/v31 v31.0.0 github.com/google/uuid v1.2.0 diff --git a/go.sum b/go.sum index 633efcc..3cde192 100644 --- a/go.sum +++ b/go.sum @@ -65,6 +65,8 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= From e1f0456228f9500bc0512f319623c5848ea58490 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 22 Feb 2021 14:57:15 +0100 Subject: [PATCH 031/502] merge --- pkg/op/mock/storage.mock.go | 50 ------------------------------------- 1 file changed, 50 deletions(-) diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index cf57014..280e8e6 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -226,56 +226,6 @@ func (mr *MockStorageMockRecorder) SaveAuthCode(arg0, arg1, arg2 interface{}) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAuthCode", reflect.TypeOf((*MockStorage)(nil).SaveAuthCode), arg0, arg1, arg2) } -// SaveNewKeyPair mocks base method -func (m *MockStorage) SaveNewKeyPair(arg0 context.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SaveNewKeyPair", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetIntrospectionFromToken mocks base method -func (m *MockStorage) SetIntrospectionFromToken(arg0 context.Context, arg1 oidc.IntrospectionResponse, arg2, arg3, arg4 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetIntrospectionFromToken", arg0, arg1, arg2, arg3, arg4) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetIntrospectionFromToken indicates an expected call of SetIntrospectionFromToken -func (mr *MockStorageMockRecorder) SetIntrospectionFromToken(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIntrospectionFromToken", reflect.TypeOf((*MockStorage)(nil).SetIntrospectionFromToken), arg0, arg1, arg2, arg3, arg4) -} - -// SetUserinfoFromScopes mocks base method -func (m *MockStorage) SetUserinfoFromScopes(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3 string, arg4 []string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetUserinfoFromScopes", arg0, arg1, arg2, arg3, arg4) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetUserinfoFromScopes indicates an expected call of SetUserinfoFromScopes -func (mr *MockStorageMockRecorder) SetUserinfoFromScopes(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserinfoFromScopes", reflect.TypeOf((*MockStorage)(nil).SetUserinfoFromScopes), arg0, arg1, arg2, arg3, arg4) -} - -// SetUserinfoFromToken mocks base method -func (m *MockStorage) SetUserinfoFromToken(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3, arg4 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetUserinfoFromToken", arg0, arg1, arg2, arg3, arg4) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetUserinfoFromToken indicates an expected call of SetUserinfoFromToken -func (mr *MockStorageMockRecorder) SetUserinfoFromToken(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserinfoFromToken", reflect.TypeOf((*MockStorage)(nil).SetUserinfoFromToken), arg0, arg1, arg2, arg3, arg4) -} - // SetIntrospectionFromToken mocks base method func (m *MockStorage) SetIntrospectionFromToken(arg0 context.Context, arg1 oidc.IntrospectionResponse, arg2, arg3, arg4 string) error { m.ctrl.T.Helper() From f2f509a522b34b14ee7c7ff9f741506ea14939ca Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Tue, 2 Mar 2021 23:58:34 +0100 Subject: [PATCH 032/502] fix: wrap original fetch key error --- pkg/op/verifier_jwt_profile.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index 03d8264..c417955 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -3,6 +3,7 @@ package op import ( "context" "errors" + "fmt" "time" "gopkg.in/square/go-jose.v2" @@ -91,7 +92,7 @@ func (k *jwtProfileKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWe } key, err := k.Storage.GetKeyByIDAndUserID(ctx, keyID, k.userID) if err != nil { - return nil, errors.New("error fetching keys") + return nil, fmt.Errorf("error fetching keys: %w", err) } payload, err, ok := oidc.CheckKey(keyID, jws, *key) if !ok { From d7d7daab2d8ac34d52e8f3a1b16db0757a1027e3 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 5 Mar 2021 07:44:37 +0100 Subject: [PATCH 033/502] fix: encoding of basic auth header values --- pkg/client/rs/resource_server.go | 4 ++-- pkg/op/token_intospection.go | 9 +++++++++ pkg/utils/http.go | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/pkg/client/rs/resource_server.go b/pkg/client/rs/resource_server.go index f5dbe69..551fe88 100644 --- a/pkg/client/rs/resource_server.go +++ b/pkg/client/rs/resource_server.go @@ -37,11 +37,11 @@ func (r *resourceServer) AuthFn() (interface{}, error) { return r.authFn() } -func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option Option) (ResourceServer, error) { +func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option ...Option) (ResourceServer, error) { authorizer := func() (interface{}, error) { return utils.AuthorizeBasic(clientID, clientSecret), nil } - return newResourceServer(issuer, authorizer, option) + return newResourceServer(issuer, authorizer, option...) } func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...Option) (ResourceServer, error) { signer, err := client.NewSignerFromPrivateKeyByte(key, keyID) diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go index 30d2544..e2ae0ad 100644 --- a/pkg/op/token_intospection.go +++ b/pkg/op/token_intospection.go @@ -3,6 +3,7 @@ package op import ( "errors" "net/http" + "net/url" "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/utils" @@ -68,6 +69,14 @@ func ParseTokenIntrospectionRequest(r *http.Request, introspector Introspector) } clientID, clientSecret, ok := r.BasicAuth() if ok { + clientID, err = url.QueryUnescape(clientID) + if err != nil { + return "", "", errors.New("invalid basic auth header") + } + clientSecret, err = url.QueryUnescape(clientSecret) + if err != nil { + return "", "", errors.New("invalid basic auth header") + } if err := introspector.Storage().AuthorizeClientIDSecret(r.Context(), clientID, clientSecret); err != nil { return "", "", err } diff --git a/pkg/utils/http.go b/pkg/utils/http.go index fa51815..6632053 100644 --- a/pkg/utils/http.go +++ b/pkg/utils/http.go @@ -30,7 +30,7 @@ type RequestAuthorization func(*http.Request) func AuthorizeBasic(user, password string) RequestAuthorization { return func(req *http.Request) { - req.SetBasicAuth(user, password) + req.SetBasicAuth(url.QueryEscape(user), url.QueryEscape(password)) } } From 8f6e2c59742f25aea2e8672c6e5454045c930d7a Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 5 Mar 2021 07:53:35 +0100 Subject: [PATCH 034/502] chore: improve signer log messages --- pkg/op/signer.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/op/signer.go b/pkg/op/signer.go index 939fe13..d59ea8e 100644 --- a/pkg/op/signer.go +++ b/pkg/op/signer.go @@ -51,9 +51,18 @@ func (s *tokenSigner) refreshSigningKey(ctx context.Context, keyCh <-chan jose.S return case key := <-keyCh: s.alg = key.Algorithm + if key.Algorithm == "" || key.Key == nil { + s.signer = nil + logging.Log("OP-DAvt4").Warn("signer has no key") + continue + } var err error s.signer, err = jose.NewSigner(key, &jose.SignerOptions{}) - logging.Log("OP-pf32aw").OnError(err).Error("error creating signer") + if err != nil { + logging.Log("OP-pf32aw").WithError(err).Error("error creating signer") + continue + } + logging.Log("OP-agRf2").Info("signer exchanged signing key") } } } From 2292d63f7bb02571f1527963cf5f6c17de342555 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Mar 2021 13:20:07 +0100 Subject: [PATCH 035/502] chore(deps): bump github.com/sirupsen/logrus from 1.8.0 to 1.8.1 (#89) Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.8.0 to 1.8.1. - [Release notes](https://github.com/sirupsen/logrus/releases) - [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md) - [Commits](https://github.com/sirupsen/logrus/compare/v1.8.0...v1.8.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 5dc5249..49abf7c 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/gorilla/schema v1.2.0 github.com/gorilla/securecookie v1.1.1 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect - github.com/sirupsen/logrus v1.8.0 + github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.7.0 golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 diff --git a/go.sum b/go.sum index 3cde192..fe24a74 100644 --- a/go.sum +++ b/go.sum @@ -133,8 +133,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g= -github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -143,8 +141,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU= -github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From 602592d5f38d39ab1d2e117d3b8f7042411e480d Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Thu, 18 Mar 2021 13:35:56 +0100 Subject: [PATCH 036/502] chore(pipeline): add Go 1.16 to matrix build (#90) * chore(pipeline): add 1.16 to matrix build * chore(readme): add GO 1.16 to supported versions --- .github/workflows/release.yml | 2 +- README.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 49016bb..e5ca513 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-18.04 strategy: matrix: - go: ['1.14', '1.15'] + go: ['1.14', '1.15', '1.16'] name: Go ${{ matrix.go }} test steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index 871b03d..41c5d85 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ For your convinience you can find the relevant standards linked below. | <1.13 | :x: | | 1.14 | :white_check_mark: | | 1.15 | :white_check_mark: | +| 1.16 | :white_check_mark: | ## Why another library From 5cd7bae50586a339403b7cf2bc0e18c56b6b703b Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Thu, 8 Apr 2021 09:54:39 +0200 Subject: [PATCH 037/502] fix: cli client (#92) * fix: cli client * fix: print shutdown error correctly --- pkg/client/rp/cli/cli.go | 15 ++++++++------- pkg/utils/http.go | 8 ++++++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/pkg/client/rp/cli/cli.go b/pkg/client/rp/cli/cli.go index 6cbb364..a00f0bd 100644 --- a/pkg/client/rp/cli/cli.go +++ b/pkg/client/rp/cli/cli.go @@ -13,13 +13,14 @@ const ( loginPath = "/login" ) -func CodeFlow(relyingParty rp.RelyingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() +func CodeFlow(ctx context.Context, relyingParty rp.RelyingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens { + codeflowCtx, codeflowCancel := context.WithCancel(ctx) + defer codeflowCancel() + + tokenChan := make(chan *oidc.Tokens, 1) - var token *oidc.Tokens callback := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string) { - token = tokens + tokenChan <- tokens msg := "

Success!

" msg = msg + "

You are authenticated and can now return to the CLI.

" w.Write([]byte(msg)) @@ -27,9 +28,9 @@ func CodeFlow(relyingParty rp.RelyingParty, callbackPath, port string, stateProv http.Handle(loginPath, rp.AuthURLHandler(stateProvider, relyingParty)) http.Handle(callbackPath, rp.CodeExchangeHandler(callback, relyingParty)) - utils.StartServer(ctx, port) + utils.StartServer(codeflowCtx, ":"+port) utils.OpenBrowser("http://localhost:" + port + loginPath) - return token + return <-tokenChan } diff --git a/pkg/utils/http.go b/pkg/utils/http.go index 6632053..27f96f9 100644 --- a/pkg/utils/http.go +++ b/pkg/utils/http.go @@ -97,7 +97,11 @@ func StartServer(ctx context.Context, port string) { go func() { <-ctx.Done() - err := server.Shutdown(ctx) - log.Fatalf("Shutdown(): %v", err) + ctxShutdown, cancelShutdown := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelShutdown() + err := server.Shutdown(ctxShutdown) + if err != nil { + log.Fatalf("Shutdown(): %v", err) + } }() } From b258b3cadb6ca2b3e3aec50050b4d8e78eb0c5c5 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 19 Apr 2021 13:41:27 +0200 Subject: [PATCH 038/502] fix: url safe encryption with no padding (#93) --- pkg/utils/crypto.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/utils/crypto.go b/pkg/utils/crypto.go index 4034f48..3ca4963 100644 --- a/pkg/utils/crypto.go +++ b/pkg/utils/crypto.go @@ -15,7 +15,7 @@ func EncryptAES(data string, key string) (string, error) { return "", err } - return base64.URLEncoding.EncodeToString(encrypted), nil + return base64.RawURLEncoding.EncodeToString(encrypted), nil } func EncryptBytesAES(plainText []byte, key string) ([]byte, error) { @@ -37,7 +37,7 @@ func EncryptBytesAES(plainText []byte, key string) ([]byte, error) { } func DecryptAES(data string, key string) (string, error) { - text, err := base64.URLEncoding.DecodeString(data) + text, err := base64.RawURLEncoding.DecodeString(data) if err != nil { return "", err } From d6cc89819baf85be72b2bef958ce59d93d6131f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Apr 2021 08:43:24 +0200 Subject: [PATCH 039/502] chore(deps): bump golang.org/x/text from 0.3.5 to 0.3.6 (#91) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.5 to 0.3.6. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.3.5...v0.3.6) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 20 ++------------------ 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index 49abf7c..b94275f 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/stretchr/testify v1.7.0 golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 - golang.org/x/text v0.3.5 + golang.org/x/text v0.3.6 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/square/go-jose.v2 v2.5.1 ) diff --git a/go.sum b/go.sum index fe24a74..371d837 100644 --- a/go.sum +++ b/go.sum @@ -63,13 +63,11 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= @@ -126,9 +124,7 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= @@ -139,12 +135,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -158,7 +152,6 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -199,7 +192,6 @@ golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -221,7 +213,6 @@ golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA= golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -253,7 +244,6 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab h1:FvshnhkKW+LO3HWHodML8kuVX8rnJTxKm9dFPuI68UM= golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -262,7 +252,6 @@ golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -274,12 +263,10 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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= @@ -325,7 +312,6 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -346,11 +332,9 @@ google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= From a2601f1584a8017d908e5394b264aec7230aedf7 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 23 Apr 2021 11:53:03 +0200 Subject: [PATCH 040/502] fix: return error when delegating user in jwt profile request (#94) --- pkg/op/verifier_jwt_profile.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index c417955..338e39a 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -69,6 +69,7 @@ func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerif if request.Issuer != request.Subject { //TODO: implement delegation (openid core / oauth rfc) + return nil, errors.New("delegation not yet implemented, issuer and sub must be identical") } keySet := &jwtProfileKeySet{v.Storage(), request.Issuer} From 72fc86164c38d23fbe5a7982a574bebfb4f58777 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 26 Apr 2021 14:31:26 +0200 Subject: [PATCH 041/502] fix: allow loopback redirect_uri for native apps --- pkg/op/authrequest.go | 65 ++++++++++++---- pkg/op/authrequest_test.go | 151 ++++++++++++++++++++++++++++++++++--- 2 files changed, 192 insertions(+), 24 deletions(-) diff --git a/pkg/op/authrequest.go b/pkg/op/authrequest.go index 3a79b9b..765027f 100644 --- a/pkg/op/authrequest.go +++ b/pkg/op/authrequest.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "net/url" "strings" "time" @@ -157,32 +158,66 @@ func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.Res if uri == "" { return ErrInvalidRequestRedirectURI("The redirect_uri is missing in the request. Please ensure it is added to the request. If you have any questions, you may contact the administrator of the application.") } + if strings.HasPrefix(uri, "https://") { + if !utils.Contains(client.RedirectURIs(), uri) { + return ErrInvalidRequestRedirectURI("The requested redirect_uri is missing in the client configuration. If you have any questions, you may contact the administrator of the application.") + } + return nil + } + if client.ApplicationType() == ApplicationTypeNative { + return validateAuthReqRedirectURINative(client, uri, responseType) + } if !utils.Contains(client.RedirectURIs(), uri) { return ErrInvalidRequestRedirectURI("The requested redirect_uri is missing in the client configuration. If you have any questions, you may contact the administrator of the application.") } - if client.DevMode() { - return nil - } - if strings.HasPrefix(uri, "https://") { - return nil - } - if responseType == oidc.ResponseTypeCode { - if strings.HasPrefix(uri, "http://") && IsConfidentialType(client) { + if strings.HasPrefix(uri, "http://") { + if client.DevMode() { return nil } - if !strings.HasPrefix(uri, "http://") && client.ApplicationType() == ApplicationTypeNative { + if responseType == oidc.ResponseTypeCode && IsConfidentialType(client) { return nil } return ErrInvalidRequest("This client's redirect_uri is http and is not allowed. If you have any questions, you may contact the administrator of the application.") - } else { - if client.ApplicationType() != ApplicationTypeNative { - return ErrInvalidRequestRedirectURI("Http is only allowed for native applications. Please change your redirect uri try again. If you have any questions, you may contact the administrator of the application.") + } + return ErrInvalidRequest("This client's redirect_uri is using a custom schema and is not allowed. If you have any questions, you may contact the administrator of the application.") +} + +//ValidateAuthReqRedirectURINative validates the passed redirect_uri and response_type to the registered uris and client type +func validateAuthReqRedirectURINative(client Client, uri string, responseType oidc.ResponseType) error { + parsedURL, isLoopback := LoopbackOrLocalhost(uri) + isCustomSchema := !strings.HasPrefix(uri, "http://") + if utils.Contains(client.RedirectURIs(), uri) { + if isLoopback || isCustomSchema { + return nil } - if !(strings.HasPrefix(uri, "http://localhost:") || strings.HasPrefix(uri, "http://localhost/")) { - return ErrInvalidRequestRedirectURI("Http is only allowed for localhost uri. Please change your redirect uri try again. If you have any questions, you may contact the administrator of the application at:") + return ErrInvalidRequest("This client's redirect_uri is http and is not allowed. If you have any questions, you may contact the administrator of the application.") + } + if !isLoopback { + return ErrInvalidRequestRedirectURI("The requested redirect_uri is missing in the client configuration. If you have any questions, you may contact the administrator of the application.") + } + for _, uri := range client.RedirectURIs() { + redirectURI, ok := LoopbackOrLocalhost(uri) + if ok && equalURI(parsedURL, redirectURI) { + return nil } } - return nil + return ErrInvalidRequestRedirectURI("The requested redirect_uri is missing in the client configuration. If you have any questions, you may contact the administrator of the application.") +} + +func equalURI(url1, url2 *url.URL) bool { + return url1.Path == url2.Path && url1.RawQuery == url2.RawQuery +} + +func LoopbackOrLocalhost(rawurl string) (*url.URL, bool) { + parsedURL, err := url.Parse(rawurl) + if err != nil { + return nil, false + } + hostName := parsedURL.Hostname() + return parsedURL, parsedURL.Scheme == "http" && + hostName == "localhost" || + hostName == "127.0.0.1" || + hostName == "::1" } //ValidateAuthReqResponseType validates the passed response_type to the registered response types diff --git a/pkg/op/authrequest_test.go b/pkg/op/authrequest_test.go index 3856acd..8427079 100644 --- a/pkg/op/authrequest_test.go +++ b/pkg/op/authrequest_test.go @@ -274,28 +274,112 @@ func TestValidateAuthReqRedirectURI(t *testing.T) { true, }, { - "unregistered fails", + "unregistered https fails", args{"https://unregistered.com/callback", mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeWeb, nil, false), oidc.ResponseTypeCode}, true, }, { - "code flow registered http not confidential fails", - args{"http://registered.com/callback", - mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, false), + "unregistered http fails", + args{"http://unregistered.com/callback", + mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeWeb, nil, false), oidc.ResponseTypeCode}, true, }, { - "code flow registered http confidential ok", + "code flow registered https web ok", + args{"https://registered.com/callback", + mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeWeb, nil, false), + oidc.ResponseTypeCode}, + false, + }, + { + "code flow registered https native ok", + args{"https://registered.com/callback", + mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeNative, nil, false), + oidc.ResponseTypeCode}, + false, + }, + { + "code flow registered https user agent ok", + args{"https://registered.com/callback", + mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, false), + oidc.ResponseTypeCode}, + false, + }, + { + "code flow registered http confidential (web) ok", args{"http://registered.com/callback", mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeWeb, nil, false), oidc.ResponseTypeCode}, false, }, { - "code flow registered custom not native fails", + "code flow registered http not confidential (user agent) fails", + args{"http://registered.com/callback", + mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, false), + oidc.ResponseTypeCode}, + true, + }, + { + "code flow registered http not confidential (native) fails", + args{"http://registered.com/callback", + mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeNative, nil, false), + oidc.ResponseTypeCode}, + true, + }, + { + "code flow registered http localhost native ok", + args{"http://localhost:4200/callback", + mock.NewClientWithConfig(t, []string{"http://localhost/callback"}, op.ApplicationTypeNative, nil, false), + oidc.ResponseTypeCode}, + false, + }, + { + "code flow registered http loopback v4 native ok", + args{"http://127.0.0.1:4200/callback", + mock.NewClientWithConfig(t, []string{"http://127.0.0.1/callback"}, op.ApplicationTypeNative, nil, false), + oidc.ResponseTypeCode}, + false, + }, + { + "code flow registered http localhost native ok", + args{"http://[::1]:4200/callback", + mock.NewClientWithConfig(t, []string{"http://[::1]/callback"}, op.ApplicationTypeNative, nil, false), + oidc.ResponseTypeCode}, + false, + }, + { + "code flow unregistered http native fails", + args{"http://unregistered.com/callback", + mock.NewClientWithConfig(t, []string{"http://locahost/callback"}, op.ApplicationTypeNative, nil, false), + oidc.ResponseTypeCode}, + true, + }, + { + "code flow unregistered custom native fails", + args{"unregistered://callback", + mock.NewClientWithConfig(t, []string{"registered://callback"}, op.ApplicationTypeNative, nil, false), + oidc.ResponseTypeCode}, + true, + }, + { + "code flow unregistered loopback native fails", + args{"http://[::1]:4200/unregistered", + mock.NewClientWithConfig(t, []string{"http://[::1]:4200/callback"}, op.ApplicationTypeNative, nil, false), + oidc.ResponseTypeCode}, + true, + }, + { + "code flow registered custom not native (web) fails", + args{"custom://callback", + mock.NewClientWithConfig(t, []string{"custom://callback"}, op.ApplicationTypeWeb, nil, false), + oidc.ResponseTypeCode}, + true, + }, + { + "code flow registered custom not native (user agent) fails", args{"custom://callback", mock.NewClientWithConfig(t, []string{"custom://callback"}, op.ApplicationTypeUserAgent, nil, false), oidc.ResponseTypeCode}, @@ -311,7 +395,7 @@ func TestValidateAuthReqRedirectURI(t *testing.T) { { "code flow dev mode http ok", args{"http://registered.com/callback", - mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeNative, nil, true), + mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, true), oidc.ResponseTypeCode}, false, }, @@ -355,12 +439,12 @@ func TestValidateAuthReqRedirectURI(t *testing.T) { args{"custom://callback", mock.NewClientWithConfig(t, []string{"custom://callback"}, op.ApplicationTypeNative, nil, false), oidc.ResponseTypeIDToken}, - true, + false, }, { "implicit flow dev mode http ok", args{"http://registered.com/callback", - mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeNative, nil, true), + mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, true), oidc.ResponseTypeIDToken}, false, }, @@ -481,3 +565,52 @@ func TestAuthResponse(t *testing.T) { }) } } + +func Test_LoopbackOrLocalhost(t *testing.T) { + type args struct { + url string + } + tests := []struct { + name string + args args + want bool + }{ + { + "v4 no port ok", + args{url: "http://127.0.0.1/test"}, + true, + }, + { + "v6 no port ok", + args{url: "http://[::1]/test"}, + true, + }, + { + "locahost no port ok", + args{url: "http://localhost/test"}, + true, + }, + { + "v4 with port ok", + args{url: "http://127.0.0.1:4200/test"}, + true, + }, + { + "v6 with port ok", + args{url: "http://[::1]:4200/test"}, + true, + }, + { + "localhost with port ok", + args{url: "http://localhost:4200/test"}, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, got := op.LoopbackOrLocalhost(tt.args.url); got != tt.want { + t.Errorf("loopbackOrLocalhost() = %v, want %v", got, tt.want) + } + }) + } +} From 5119d7aea308f974c24c99fbbeaded36db78187f Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Thu, 29 Apr 2021 09:20:01 +0200 Subject: [PATCH 042/502] begin refresh token --- example/internal/mock/storage.go | 6 + go.sum | 54 ++++ pkg/client/rp/relaying_party.go | 1 - pkg/oidc/token_request.go | 36 +++ pkg/op/{authrequest.go => auth_request.go} | 3 +- ...threquest_test.go => auth_request_test.go} | 0 pkg/op/storage.go | 2 + pkg/op/token.go | 87 ++++--- pkg/op/token_code.go | 107 ++++++++ pkg/op/token_exchange.go | 30 +++ pkg/op/token_jwt_profile.go | 79 ++++++ pkg/op/token_refresh.go | 113 ++++++++ pkg/op/token_request.go | 121 +++++++++ pkg/op/tokenrequest.go | 242 ------------------ pkg/op/verifier_jwt_profile.go | 5 + 15 files changed, 611 insertions(+), 275 deletions(-) rename pkg/op/{authrequest.go => auth_request.go} (98%) rename pkg/op/{authrequest_test.go => auth_request_test.go} (100%) create mode 100644 pkg/op/token_code.go create mode 100644 pkg/op/token_exchange.go create mode 100644 pkg/op/token_jwt_profile.go create mode 100644 pkg/op/token_refresh.go create mode 100644 pkg/op/token_request.go delete mode 100644 pkg/op/tokenrequest.go diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index d00d7a5..247459e 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -154,6 +154,12 @@ func (s *AuthStorage) AuthRequestByID(_ context.Context, id string) (op.AuthRequ func (s *AuthStorage) CreateToken(_ context.Context, authReq op.TokenRequest) (string, time.Time, error) { return "id", time.Now().UTC().Add(5 * time.Minute), nil } +func (s *AuthStorage) AuthRequestByRefreshToken(_ context.Context, token string) (op.AuthRequest, error) { + if token != c { + return nil, errors.New("invalid token") + } + return a, nil +} func (s *AuthStorage) TerminateSession(_ context.Context, userID, clientID string) error { return nil } diff --git a/go.sum b/go.sum index 371d837..bb48d24 100644 --- a/go.sum +++ b/go.sum @@ -12,50 +12,70 @@ cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bP cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/caos/logging v0.0.2 h1:ebg5C/HN0ludYR+WkvnFjwSExF4wvyiWPyWGcKMYsoo= github.com/caos/logging v0.0.2/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0= +github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f h1:WBZRG4aNOuI15bLRrCgN8fCq8E5Xuty6jGbmSNEvSsU= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4 h1:rEvIZUSZ3fx39WIi3JkQqQBitGwpELBIYWeBVh6wn+E= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= 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 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -81,6 +101,7 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -95,7 +116,9 @@ github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6C github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0 h1:pMen7vLs8nvgEYhywH3KDWJIJTeEr2ULsVWHWYHQyBs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -103,11 +126,14 @@ github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99 h1:Ak8CrdlwwXwAZxzS66vgPt4U8yUZX7JwLvVR58FN5jM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= @@ -118,14 +144,21 @@ github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlI github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -133,12 +166,15 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -146,11 +182,13 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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 h1:5tjfNdR2ki3yYQ842+eX2sQHeiwpKJ0RnHO4IYOc4V8= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 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= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -167,8 +205,10 @@ golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -179,14 +219,17 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -230,6 +273,7 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -269,6 +313,7 @@ golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/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= @@ -309,6 +354,7 @@ golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -330,6 +376,7 @@ google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -366,6 +413,7 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 h1:PDIOdWxZ8eRizhKa1AAvY53xsvLB1cWorMjslvY3VA8= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -378,6 +426,7 @@ google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 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 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 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= @@ -394,6 +443,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= @@ -408,7 +458,11 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/pkg/client/rp/relaying_party.go b/pkg/client/rp/relaying_party.go index 528f554..5a48951 100644 --- a/pkg/client/rp/relaying_party.go +++ b/pkg/client/rp/relaying_party.go @@ -165,7 +165,6 @@ func NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, sco return nil, err } } - endpoints, err := Discover(rp.issuer, rp.httpClient) if err != nil { return nil, err diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index 0c5b70b..1136f8e 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -10,6 +10,9 @@ const ( //GrantTypeCode defines the grant_type `authorization_code` used for the Token Request in the Authorization Code Flow GrantTypeCode GrantType = "authorization_code" + //GrantTypeCode defines the grant_type `refresh_token` used for the Token Request in the Refresh Token Flow + GrantTypeRefreshToken GrantType = "refresh_token" + //GrantTypeBearer defines the grant_type `urn:ietf:params:oauth:grant-type:jwt-bearer` used for the JWT Authorization Grant GrantTypeBearer GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" @@ -44,6 +47,39 @@ func (a *AccessTokenRequest) GrantType() GrantType { return GrantTypeCode } +//SetClientID implements op.AuthenticatedTokenRequest +func (a *AccessTokenRequest) SetClientID(clientID string) { + a.ClientID = clientID +} + +//SetClientSecret implements op.AuthenticatedTokenRequest +func (a *AccessTokenRequest) SetClientSecret(clientSecret string) { + a.ClientSecret = clientSecret +} + +type RefreshTokenRequest struct { + RefreshToken string `schema:"refresh_token"` + Scopes Scopes `schema:"scope"` + ClientID string `schema:"client_id"` + ClientSecret string `schema:"client_secret"` + ClientAssertion string `schema:"client_assertion"` + ClientAssertionType string `schema:"client_assertion_type"` +} + +func (a *RefreshTokenRequest) GrantType() GrantType { + return GrantTypeRefreshToken +} + +//SetClientID implements op.AuthenticatedTokenRequest +func (a *RefreshTokenRequest) SetClientID(clientID string) { + a.ClientID = clientID +} + +//SetClientSecret implements op.AuthenticatedTokenRequest +func (a *RefreshTokenRequest) SetClientSecret(clientSecret string) { + a.ClientSecret = clientSecret +} + type JWTTokenRequest struct { Issuer string `json:"iss"` Subject string `json:"sub"` diff --git a/pkg/op/authrequest.go b/pkg/op/auth_request.go similarity index 98% rename from pkg/op/authrequest.go rename to pkg/op/auth_request.go index 3a79b9b..dcb5e39 100644 --- a/pkg/op/authrequest.go +++ b/pkg/op/auth_request.go @@ -89,7 +89,7 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { RedirectToLogin(req.GetID(), client, w, r) } -//ParseAuthorizeRequest parsed the http request into a AuthRequest +//ParseAuthorizeRequest parsed the http request into a oidc.AuthRequest func ParseAuthorizeRequest(r *http.Request, decoder utils.Decoder) (*oidc.AuthRequest, error) { err := r.ParseForm() if err != nil { @@ -289,6 +289,7 @@ func CreateAuthRequestCode(ctx context.Context, authReq AuthRequest, storage Sto return code, nil } +//BuildAuthRequestCode builds the string representation of the auth code func BuildAuthRequestCode(authReq AuthRequest, crypto Crypto) (string, error) { return crypto.Encrypt(authReq.GetID()) } diff --git a/pkg/op/authrequest_test.go b/pkg/op/auth_request_test.go similarity index 100% rename from pkg/op/authrequest_test.go rename to pkg/op/auth_request_test.go diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 277f244..adba5cf 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -17,6 +17,8 @@ type AuthStorage interface { DeleteAuthRequest(context.Context, string) error CreateToken(context.Context, TokenRequest) (string, time.Time, error) + CreateTokens(ctx context.Context, request TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) + RefreshTokenRequestByRefreshToken(context.Context, string) (RefreshTokenRequest, error) TerminateSession(context.Context, string, string) error diff --git a/pkg/op/token.go b/pkg/op/token.go index 334bec9..8592003 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -21,53 +21,61 @@ type TokenRequest interface { GetScopes() []string } -func CreateTokenResponse(ctx context.Context, authReq AuthRequest, client Client, creator TokenCreator, createAccessToken bool, code string) (*oidc.AccessTokenResponse, error) { - var accessToken string +func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Client, creator TokenCreator, createAccessToken bool, code string) (*oidc.AccessTokenResponse, error) { + var accessToken, refreshToken string var validity time.Duration if createAccessToken { var err error - accessToken, validity, err = CreateAccessToken(ctx, authReq, client.AccessTokenType(), creator, client) + accessToken, refreshToken, validity, err = CreateAccessToken(ctx, request, client.AccessTokenType(), creator, client) if err != nil { return nil, err } } - idToken, err := CreateIDToken(ctx, creator.Issuer(), authReq, client.IDTokenLifetime(), accessToken, code, creator.Storage(), creator.Signer(), client) + idToken, err := CreateIDToken(ctx, creator.Issuer(), request, client.IDTokenLifetime(), accessToken, code, creator.Storage(), creator.Signer(), client) if err != nil { return nil, err } - err = creator.Storage().DeleteAuthRequest(ctx, authReq.GetID()) - if err != nil { - return nil, err + if authRequest, ok := request.(AuthRequest); ok { + err = creator.Storage().DeleteAuthRequest(ctx, authRequest.GetID()) + if err != nil { + return nil, err + } } exp := uint64(validity.Seconds()) return &oidc.AccessTokenResponse{ - AccessToken: accessToken, - IDToken: idToken, - TokenType: oidc.BearerToken, - ExpiresIn: exp, + AccessToken: accessToken, + IDToken: idToken, + RefreshToken: refreshToken, + TokenType: oidc.BearerToken, + ExpiresIn: exp, }, nil } -func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator) (*oidc.AccessTokenResponse, error) { - accessToken, validity, err := CreateAccessToken(ctx, tokenRequest, AccessTokenTypeBearer, creator, nil) - if err != nil { - return nil, err +func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storage) (id, refreshToken string, exp time.Time, err error) { + if needsRefreshToken(tokenRequest) { + id, exp, err = storage.CreateToken(ctx, tokenRequest) + return } - - exp := uint64(validity.Seconds()) - return &oidc.AccessTokenResponse{ - AccessToken: accessToken, - TokenType: oidc.BearerToken, - ExpiresIn: exp, - }, nil + return storage.CreateTokens(ctx, tokenRequest, "hodor") } -func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTokenType AccessTokenType, creator TokenCreator, client Client) (token string, validity time.Duration, err error) { - id, exp, err := creator.Storage().CreateToken(ctx, tokenRequest) +func needsRefreshToken(tokenRequest TokenRequest) bool { + switch req := tokenRequest.(type) { + case AuthRequest: + return utils.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && req.GetResponseType() == oidc.ResponseTypeCode + case RefreshTokenRequest: + return true + default: + return false + } +} + +func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTokenType AccessTokenType, creator TokenCreator, client Client) (accessToken, refreshToken string, validity time.Duration, err error) { + id, refreshToken, exp, err := createTokens(ctx, tokenRequest, creator.Storage()) if err != nil { - return "", 0, err + return "", "", 0, err } var clockSkew time.Duration if client != nil { @@ -75,10 +83,10 @@ func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTok } validity = exp.Add(clockSkew).Sub(time.Now().UTC()) if accessTokenType == AccessTokenTypeJWT { - token, err = CreateJWT(ctx, creator.Issuer(), tokenRequest, exp, id, creator.Signer(), client, creator.Storage()) + accessToken, err = CreateJWT(ctx, creator.Issuer(), tokenRequest, exp, id, creator.Signer(), client, creator.Storage()) return } - token, err = CreateBearerToken(id, tokenRequest.GetSubject(), creator.Crypto()) + accessToken, err = CreateBearerToken(id, tokenRequest.GetSubject(), creator.Crypto()) return } @@ -99,10 +107,27 @@ func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, ex return utils.Sign(claims, signer.Signer()) } -func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, validity time.Duration, accessToken, code string, storage Storage, signer Signer, client Client) (string, error) { +type IDTokenRequest interface { + //GetACR() string + //GetAMR() []string + GetAudience() []string + GetAuthTime() time.Time + GetClientID() string + GetScopes() []string + GetSubject() string +} + +func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, validity time.Duration, accessToken, code string, storage Storage, signer Signer, client Client) (string, error) { exp := time.Now().UTC().Add(client.ClockSkew()).Add(validity) - claims := oidc.NewIDTokenClaims(issuer, authReq.GetSubject(), authReq.GetAudience(), exp, authReq.GetAuthTime(), authReq.GetNonce(), authReq.GetACR(), authReq.GetAMR(), authReq.GetClientID(), client.ClockSkew()) - scopes := client.RestrictAdditionalIdTokenScopes()(authReq.GetScopes()) + var acr, nonce string + var amr []string + if authRequest, ok := request.(AuthRequest); ok { + acr = authRequest.GetACR() + amr = authRequest.GetAMR() + nonce = authRequest.GetNonce() + } + claims := oidc.NewIDTokenClaims(issuer, request.GetSubject(), request.GetAudience(), exp, request.GetAuthTime(), nonce, acr, amr, request.GetClientID(), client.ClockSkew()) + scopes := client.RestrictAdditionalIdTokenScopes()(request.GetScopes()) if accessToken != "" { atHash, err := oidc.ClaimHash(accessToken, signer.SignatureAlgorithm()) if err != nil { @@ -115,7 +140,7 @@ func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, vali } if len(scopes) > 0 { userInfo := oidc.NewUserInfo() - err := storage.SetUserinfoFromScopes(ctx, userInfo, authReq.GetSubject(), authReq.GetClientID(), scopes) + err := storage.SetUserinfoFromScopes(ctx, userInfo, request.GetSubject(), request.GetClientID(), scopes) if err != nil { return "", err } diff --git a/pkg/op/token_code.go b/pkg/op/token_code.go new file mode 100644 index 0000000..0f27104 --- /dev/null +++ b/pkg/op/token_code.go @@ -0,0 +1,107 @@ +package op + +import ( + "context" + "errors" + "net/http" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +//CodeExchange handles the OAuth 2.0 authorization_code grant, including +//parsing, validating, authorizing the client and finally exchanging the code for tokens +func CodeExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { + tokenReq, err := ParseAccessTokenRequest(r, exchanger.Decoder()) + if err != nil { + RequestError(w, r, err) + } + if tokenReq.Code == "" { + RequestError(w, r, ErrInvalidRequest("code missing")) + return + } + authReq, client, err := ValidateAccessTokenRequest(r.Context(), tokenReq, exchanger) + if err != nil { + RequestError(w, r, err) + return + } + resp, err := CreateTokenResponse(r.Context(), authReq, client, exchanger, true, tokenReq.Code) + if err != nil { + RequestError(w, r, err) + return + } + utils.MarshalJSON(w, resp) +} + +//ParseAccessTokenRequest parsed the http request into a oidc.AccessTokenRequest +func ParseAccessTokenRequest(r *http.Request, decoder utils.Decoder) (*oidc.AccessTokenRequest, error) { + request := new(oidc.AccessTokenRequest) + err := ParseAuthenticatedTokenRequest(r, decoder, request) + if err != nil { + return nil, err + } + return request, nil +} + +//ValidateAccessTokenRequest validates the token request parameters including authorization check of the client +//and returns the previous created auth request corresponding to the auth code +func ValidateAccessTokenRequest(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (AuthRequest, Client, error) { + authReq, client, err := AuthorizeCodeClient(ctx, tokenReq, exchanger) + if err != nil { + return nil, nil, err + } + if client.GetID() != authReq.GetClientID() { + return nil, nil, ErrInvalidRequest("invalid auth code") + } + if tokenReq.RedirectURI != authReq.GetRedirectURI() { + return nil, nil, ErrInvalidRequest("redirect_uri does no correspond") + } + return authReq, client, nil +} + +//AuthorizeCodeClient checks the authorization of the client and that the used method was the one previously registered. +//It than returns the auth request corresponding to the auth code +func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (request AuthRequest, client Client, err error) { + if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion { + jwtExchanger, ok := exchanger.(JWTAuthorizationGrantExchanger) + if !ok || !exchanger.AuthMethodPrivateKeyJWTSupported() { + return nil, nil, errors.New("auth_method private_key_jwt not supported") + } + client, err = AuthorizePrivateJWTKey(ctx, tokenReq.ClientAssertion, jwtExchanger) + if err != nil { + return nil, nil, err + } + request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code) + return request, client, err + } + client, err = exchanger.Storage().GetClientByClientID(ctx, tokenReq.ClientID) + if err != nil { + return nil, nil, err + } + if client.AuthMethod() == oidc.AuthMethodPrivateKeyJWT { + return nil, nil, errors.New("invalid_grant") + } + if client.AuthMethod() == oidc.AuthMethodNone { + request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code) + if err != nil { + return nil, nil, err + } + err = AuthorizeCodeChallenge(tokenReq, request.GetCodeChallenge()) + return request, client, err + } + if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() { + return nil, nil, errors.New("auth_method post not supported") + } + err = AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, exchanger.Storage()) + request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code) + return request, client, err +} + +//AuthRequestByCode returns the AuthRequest previously created from Storage corresponding to the auth code or an error +func AuthRequestByCode(ctx context.Context, storage Storage, code string) (AuthRequest, error) { + authReq, err := storage.AuthRequestByCode(ctx, code) + if err != nil { + return nil, ErrInvalidRequest("invalid code") + } + return authReq, nil +} diff --git a/pkg/op/token_exchange.go b/pkg/op/token_exchange.go new file mode 100644 index 0000000..8d93e0c --- /dev/null +++ b/pkg/op/token_exchange.go @@ -0,0 +1,30 @@ +package op + +import ( + "errors" + "net/http" + + "github.com/caos/oidc/pkg/oidc" +) + +//TokenExchange will handle the OAuth 2.0 token exchange grant ("urn:ietf:params:oauth:grant-type:token-exchange") +func TokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { + tokenRequest, err := ParseTokenExchangeRequest(w, r) + if err != nil { + RequestError(w, r, err) + return + } + err = ValidateTokenExchangeRequest(tokenRequest, exchanger.Storage()) + if err != nil { + RequestError(w, r, err) + return + } +} + +func ParseTokenExchangeRequest(w http.ResponseWriter, r *http.Request) (oidc.TokenRequest, error) { + return nil, errors.New("Unimplemented") //TODO: impl +} + +func ValidateTokenExchangeRequest(tokenReq oidc.TokenRequest, storage Storage) error { + return errors.New("Unimplemented") //TODO: impl +} diff --git a/pkg/op/token_jwt_profile.go b/pkg/op/token_jwt_profile.go new file mode 100644 index 0000000..c96d4b2 --- /dev/null +++ b/pkg/op/token_jwt_profile.go @@ -0,0 +1,79 @@ +package op + +import ( + "context" + "net/http" + "time" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +type JWTAuthorizationGrantExchanger interface { + Exchanger + JWTProfileVerifier() JWTProfileVerifier +} + +//JWTProfile handles the OAuth 2.0 JWT Profile Authorization Grant https://tools.ietf.org/html/rfc7523#section-2.1 +func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizationGrantExchanger) { + profileRequest, err := ParseJWTProfileGrantRequest(r, exchanger.Decoder()) + if err != nil { + RequestError(w, r, err) + } + + tokenRequest, err := VerifyJWTAssertion(r.Context(), profileRequest.Assertion, exchanger.JWTProfileVerifier()) + if err != nil { + RequestError(w, r, err) + return + } + + tokenRequest.Scopes, err = exchanger.Storage().ValidateJWTProfileScopes(r.Context(), tokenRequest.Issuer, profileRequest.Scope) + if err != nil { + RequestError(w, r, err) + return + } + resp, err := CreateJWTTokenResponse(r.Context(), tokenRequest, exchanger) + if err != nil { + RequestError(w, r, err) + return + } + utils.MarshalJSON(w, resp) +} + +func ParseJWTProfileGrantRequest(r *http.Request, decoder utils.Decoder) (*oidc.JWTProfileGrantRequest, error) { + err := r.ParseForm() + if err != nil { + return nil, ErrInvalidRequest("error parsing form") + } + tokenReq := new(oidc.JWTProfileGrantRequest) + err = decoder.Decode(tokenReq, r.Form) + if err != nil { + return nil, ErrInvalidRequest("error decoding form") + } + return tokenReq, nil +} + +//CreateJWTTokenResponse creates +func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator) (*oidc.AccessTokenResponse, error) { + id, exp, err := creator.Storage().CreateToken(ctx, tokenRequest) + if err != nil { + return nil, err + } + accessToken, err := CreateBearerToken(id, tokenRequest.GetSubject(), creator.Crypto()) + if err != nil { + return nil, err + } + + return &oidc.AccessTokenResponse{ + AccessToken: accessToken, + TokenType: oidc.BearerToken, + ExpiresIn: uint64(exp.Sub(time.Now().UTC()).Seconds()), + }, nil +} + +//ParseJWTProfileRequest has been renamed to ParseJWTProfileGrantRequest +// +//deprecated: use ParseJWTProfileGrantRequest +func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (*oidc.JWTProfileGrantRequest, error) { + return ParseJWTProfileGrantRequest(r, decoder) +} diff --git a/pkg/op/token_refresh.go b/pkg/op/token_refresh.go new file mode 100644 index 0000000..5f3c3a5 --- /dev/null +++ b/pkg/op/token_refresh.go @@ -0,0 +1,113 @@ +package op + +import ( + "context" + "errors" + "net/http" + "time" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +type RefreshTokenRequest interface { + //GetID() string + //GetACR() string + //GetAMR() []string + GetAudience() []string + GetAuthTime() time.Time + GetClientID() string + GetScopes() []string + GetSubject() string + //GetRefreshToken() string +} + +//RefreshTokenExchange handles the OAuth 2.0 refresh_token grant, including +//parsing, validating, authorizing the client and finally exchanging the refresh_token for new tokens +func RefreshTokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { + tokenReq, err := ParseRefreshTokenRequest(r, exchanger.Decoder()) + if err != nil { + RequestError(w, r, err) + } + authReq, client, err := ValidateRefreshTokenRequest(r.Context(), tokenReq, exchanger) + if err != nil { + RequestError(w, r, err) + return + } + resp, err := CreateTokenResponse(r.Context(), authReq, client, exchanger, true, "") + if err != nil { + RequestError(w, r, err) + return + } + utils.MarshalJSON(w, resp) +} + +//ParseRefreshTokenRequest parsed the http request into a oidc.RefreshTokenRequest +func ParseRefreshTokenRequest(r *http.Request, decoder utils.Decoder) (*oidc.RefreshTokenRequest, error) { + request := new(oidc.RefreshTokenRequest) + err := ParseAuthenticatedTokenRequest(r, decoder, request) + if err != nil { + return nil, err + } + return request, nil +} + +//ValidateRefreshTokenRequest validates the refresh_token request parameters including authorization check of the client +//and returns the data representing the original auth request corresponding to the refresh_token +func ValidateRefreshTokenRequest(ctx context.Context, tokenReq *oidc.RefreshTokenRequest, exchanger Exchanger) (RefreshTokenRequest, Client, error) { + if tokenReq.RefreshToken == "" { + return nil, nil, ErrInvalidRequest("code missing") + } + authReq, client, err := AuthorizeRefreshClient(ctx, tokenReq, exchanger) + if err != nil { + return nil, nil, err + } + if client.GetID() != authReq.GetClientID() { + return nil, nil, ErrInvalidRequest("invalid auth code") + } + return authReq, client, nil +} + +//AuthorizeCodeClient checks the authorization of the client and that the used method was the one previously registered. +//It than returns the data representing the original auth request corresponding to the refresh_token +func AuthorizeRefreshClient(ctx context.Context, tokenReq *oidc.RefreshTokenRequest, exchanger Exchanger) (request RefreshTokenRequest, client Client, err error) { + if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion { + jwtExchanger, ok := exchanger.(JWTAuthorizationGrantExchanger) + if !ok || !exchanger.AuthMethodPrivateKeyJWTSupported() { + return nil, nil, errors.New("auth_method private_key_jwt not supported") + } + client, err = AuthorizePrivateJWTKey(ctx, tokenReq.ClientAssertion, jwtExchanger) + if err != nil { + return nil, nil, err + } + request, err = RefreshTokenRequestByRefreshToken(ctx, exchanger.Storage(), tokenReq.RefreshToken) + return request, client, err + } + client, err = exchanger.Storage().GetClientByClientID(ctx, tokenReq.ClientID) + if err != nil { + return nil, nil, err + } + if client.AuthMethod() == oidc.AuthMethodPrivateKeyJWT { + return nil, nil, errors.New("invalid_grant") + } + if client.AuthMethod() == oidc.AuthMethodNone { + request, err = RefreshTokenRequestByRefreshToken(ctx, exchanger.Storage(), tokenReq.RefreshToken) + return request, client, err + } + if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() { + return nil, nil, errors.New("auth_method post not supported") + } + err = AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, exchanger.Storage()) + request, err = RefreshTokenRequestByRefreshToken(ctx, exchanger.Storage(), tokenReq.RefreshToken) + return request, client, err +} + +//RefreshTokenRequestByRefreshToken returns the RefreshTokenRequest (data representing the original auth request) +//corresponding to the refresh_token from Storage or an error +func RefreshTokenRequestByRefreshToken(ctx context.Context, storage Storage, refreshToken string) (RefreshTokenRequest, error) { + authReq, err := storage.RefreshTokenRequestByRefreshToken(ctx, refreshToken) + if err != nil { + return nil, ErrInvalidRequest("invalid refreshToken") + } + return authReq, nil +} diff --git a/pkg/op/token_request.go b/pkg/op/token_request.go new file mode 100644 index 0000000..f8148d7 --- /dev/null +++ b/pkg/op/token_request.go @@ -0,0 +1,121 @@ +package op + +import ( + "context" + "net/http" + "net/url" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +type Exchanger interface { + Issuer() string + Storage() Storage + Decoder() utils.Decoder + Signer() Signer + Crypto() Crypto + AuthMethodPostSupported() bool + AuthMethodPrivateKeyJWTSupported() bool + GrantTypeTokenExchangeSupported() bool + GrantTypeJWTAuthorizationSupported() bool +} + +func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + switch r.FormValue("grant_type") { + case string(oidc.GrantTypeCode): + CodeExchange(w, r, exchanger) + return + case string(oidc.GrantTypeRefreshToken): + RefreshTokenExchange(w, r, exchanger) + return + case string(oidc.GrantTypeBearer): + if ex, ok := exchanger.(JWTAuthorizationGrantExchanger); ok && exchanger.GrantTypeJWTAuthorizationSupported() { + JWTProfile(w, r, ex) + return + } + case string(oidc.GrantTypeTokenExchange): + if exchanger.GrantTypeTokenExchangeSupported() { + TokenExchange(w, r, exchanger) + return + } + case "": + RequestError(w, r, ErrInvalidRequest("grant_type missing")) + return + } + RequestError(w, r, ErrInvalidRequest("grant_type not supported")) + } +} + +//authenticatedTokenRequest is a helper interface for ParseAuthenticatedTokenRequest +//it is implemented by oidc.AuthRequest and oidc.RefreshTokenRequest +type AuthenticatedTokenRequest interface { + SetClientID(string) + SetClientSecret(string) +} + +//ParseAuthenticatedTokenRequest parses the client_id and client_secret from the HTTP request from either +//HTTP Basic Auth header or form body and sets them into the provided authenticatedTokenRequest interface +func ParseAuthenticatedTokenRequest(r *http.Request, decoder utils.Decoder, request AuthenticatedTokenRequest) error { + err := r.ParseForm() + if err != nil { + return ErrInvalidRequest("error parsing form") + } + err = decoder.Decode(request, r.Form) + if err != nil { + return ErrInvalidRequest("error decoding form") + } + clientID, clientSecret, ok := r.BasicAuth() + if ok { + clientID, err = url.QueryUnescape(clientID) + if err != nil { + return ErrInvalidRequest("invalid basic auth header") + } + clientSecret, err = url.QueryUnescape(clientSecret) + if err != nil { + return ErrInvalidRequest("invalid basic auth header") + } + request.SetClientID(clientID) + request.SetClientSecret(clientSecret) + } + return nil +} + +//AuthorizeRefreshClientByClientIDSecret authorizes a client by validating the client_id and client_secret (Basic Auth and POST) +func AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string, storage Storage) error { + err := storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret) + if err != nil { + return err //TODO: wrap? + } + return nil +} + +//AuthorizeCodeClientByCodeChallenge authorizes a client by validating the code_verifier against the previously sent +//code_challenge of the auth request (PKCE) +func AuthorizeCodeChallenge(tokenReq *oidc.AccessTokenRequest, challenge *oidc.CodeChallenge) error { + if tokenReq.CodeVerifier == "" { + return ErrInvalidRequest("code_challenge required") + } + if !oidc.VerifyCodeChallenge(challenge, tokenReq.CodeVerifier) { + return ErrInvalidRequest("code_challenge invalid") + } + return nil +} + +//AuthorizePrivateJWTKey authorizes a client by validating the client_assertion's signature with a previously +//registered public key (JWT Profile) +func AuthorizePrivateJWTKey(ctx context.Context, clientAssertion string, exchanger JWTAuthorizationGrantExchanger) (Client, error) { + jwtReq, err := VerifyJWTAssertion(ctx, clientAssertion, exchanger.JWTProfileVerifier()) + if err != nil { + return nil, err + } + client, err := exchanger.Storage().GetClientByClientID(ctx, jwtReq.Issuer) + if err != nil { + return nil, err + } + if client.AuthMethod() != oidc.AuthMethodPrivateKeyJWT { + return nil, ErrInvalidRequest("invalid_client") + } + return client, nil +} diff --git a/pkg/op/tokenrequest.go b/pkg/op/tokenrequest.go deleted file mode 100644 index b51d2c8..0000000 --- a/pkg/op/tokenrequest.go +++ /dev/null @@ -1,242 +0,0 @@ -package op - -import ( - "context" - "errors" - "net/http" - "net/url" - - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" -) - -type Exchanger interface { - Issuer() string - Storage() Storage - Decoder() utils.Decoder - Signer() Signer - Crypto() Crypto - AuthMethodPostSupported() bool - AuthMethodPrivateKeyJWTSupported() bool - GrantTypeTokenExchangeSupported() bool - GrantTypeJWTAuthorizationSupported() bool -} - -type JWTAuthorizationGrantExchanger interface { - Exchanger - JWTProfileVerifier() JWTProfileVerifier -} - -func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - switch r.FormValue("grant_type") { - case string(oidc.GrantTypeCode): - CodeExchange(w, r, exchanger) - return - case string(oidc.GrantTypeBearer): - if ex, ok := exchanger.(JWTAuthorizationGrantExchanger); ok && exchanger.GrantTypeJWTAuthorizationSupported() { - JWTProfile(w, r, ex) - return - } - case string(oidc.GrantTypeTokenExchange): - if exchanger.GrantTypeTokenExchangeSupported() { - TokenExchange(w, r, exchanger) - return - } - case "": - RequestError(w, r, ErrInvalidRequest("grant_type missing")) - return - } - RequestError(w, r, ErrInvalidRequest("grant_type not supported")) - } -} - -func CodeExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { - tokenReq, err := ParseAccessTokenRequest(r, exchanger.Decoder()) - if err != nil { - RequestError(w, r, err) - } - if tokenReq.Code == "" { - RequestError(w, r, ErrInvalidRequest("code missing")) - return - } - authReq, client, err := ValidateAccessTokenRequest(r.Context(), tokenReq, exchanger) - if err != nil { - RequestError(w, r, err) - return - } - resp, err := CreateTokenResponse(r.Context(), authReq, client, exchanger, true, tokenReq.Code) - if err != nil { - RequestError(w, r, err) - return - } - utils.MarshalJSON(w, resp) -} - -func ParseAccessTokenRequest(r *http.Request, decoder utils.Decoder) (*oidc.AccessTokenRequest, error) { - err := r.ParseForm() - if err != nil { - return nil, ErrInvalidRequest("error parsing form") - } - tokenReq := new(oidc.AccessTokenRequest) - err = decoder.Decode(tokenReq, r.Form) - if err != nil { - return nil, ErrInvalidRequest("error decoding form") - } - clientID, clientSecret, ok := r.BasicAuth() - if ok { - tokenReq.ClientID, err = url.QueryUnescape(clientID) - if err != nil { - return nil, ErrInvalidRequest("invalid basic auth header") - } - tokenReq.ClientSecret, err = url.QueryUnescape(clientSecret) - if err != nil { - return nil, ErrInvalidRequest("invalid basic auth header") - } - } - return tokenReq, nil -} - -func ValidateAccessTokenRequest(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (AuthRequest, Client, error) { - authReq, client, err := AuthorizeClient(ctx, tokenReq, exchanger) - if err != nil { - return nil, nil, err - } - if client.GetID() != authReq.GetClientID() { - return nil, nil, ErrInvalidRequest("invalid auth code") - } - if tokenReq.RedirectURI != authReq.GetRedirectURI() { - return nil, nil, ErrInvalidRequest("redirect_uri does no correspond") - } - return authReq, client, nil -} - -func AuthorizeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (AuthRequest, Client, error) { - if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion { - jwtExchanger, ok := exchanger.(JWTAuthorizationGrantExchanger) - if !ok || !exchanger.AuthMethodPrivateKeyJWTSupported() { - return nil, nil, errors.New("auth_method private_key_jwt not supported") - } - return AuthorizePrivateJWTKey(ctx, tokenReq, jwtExchanger) - } - client, err := exchanger.Storage().GetClientByClientID(ctx, tokenReq.ClientID) - if err != nil { - return nil, nil, err - } - if client.AuthMethod() == oidc.AuthMethodPrivateKeyJWT { - return nil, nil, errors.New("invalid_grant") - } - if client.AuthMethod() == oidc.AuthMethodNone { - authReq, err := AuthorizeCodeChallenge(ctx, tokenReq, exchanger) - return authReq, client, err - } - if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() { - return nil, nil, errors.New("auth_method post not supported") - } - authReq, err := AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, tokenReq.Code, exchanger.Storage()) - return authReq, client, err -} - -func AuthorizePrivateJWTKey(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger JWTAuthorizationGrantExchanger) (AuthRequest, Client, error) { - jwtReq, err := VerifyJWTAssertion(ctx, tokenReq.ClientAssertion, exchanger.JWTProfileVerifier()) - if err != nil { - return nil, nil, err - } - authReq, err := exchanger.Storage().AuthRequestByCode(ctx, tokenReq.Code) - if err != nil { - return nil, nil, ErrInvalidRequest("invalid code") - } - client, err := exchanger.Storage().GetClientByClientID(ctx, jwtReq.Issuer) - if err != nil { - return nil, nil, err - } - if client.AuthMethod() != oidc.AuthMethodPrivateKeyJWT { - return nil, nil, ErrInvalidRequest("invalid_client") - } - return authReq, client, nil -} - -func AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret, code string, storage Storage) (AuthRequest, error) { - err := storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret) - if err != nil { - return nil, err - } - authReq, err := storage.AuthRequestByCode(ctx, code) - if err != nil { - return nil, ErrInvalidRequest("invalid code") - } - return authReq, nil -} - -func AuthorizeCodeChallenge(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (AuthRequest, error) { - if tokenReq.CodeVerifier == "" { - return nil, ErrInvalidRequest("code_challenge required") - } - authReq, err := exchanger.Storage().AuthRequestByCode(ctx, tokenReq.Code) - if err != nil { - return nil, ErrInvalidRequest("invalid code") - } - if !oidc.VerifyCodeChallenge(authReq.GetCodeChallenge(), tokenReq.CodeVerifier) { - return nil, ErrInvalidRequest("code_challenge invalid") - } - return authReq, nil -} - -func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizationGrantExchanger) { - profileRequest, err := ParseJWTProfileRequest(r, exchanger.Decoder()) - if err != nil { - RequestError(w, r, err) - } - - tokenRequest, err := VerifyJWTAssertion(r.Context(), profileRequest.Assertion, exchanger.JWTProfileVerifier()) - if err != nil { - RequestError(w, r, err) - return - } - - tokenRequest.Scopes, err = exchanger.Storage().ValidateJWTProfileScopes(r.Context(), tokenRequest.Issuer, profileRequest.Scope) - if err != nil { - RequestError(w, r, err) - return - } - resp, err := CreateJWTTokenResponse(r.Context(), tokenRequest, exchanger) - if err != nil { - RequestError(w, r, err) - return - } - utils.MarshalJSON(w, resp) -} - -func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (*oidc.JWTProfileGrantRequest, error) { - err := r.ParseForm() - if err != nil { - return nil, ErrInvalidRequest("error parsing form") - } - tokenReq := new(oidc.JWTProfileGrantRequest) - err = decoder.Decode(tokenReq, r.Form) - if err != nil { - return nil, ErrInvalidRequest("error decoding form") - } - return tokenReq, nil -} - -func TokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { - tokenRequest, err := ParseTokenExchangeRequest(w, r) - if err != nil { - RequestError(w, r, err) - return - } - err = ValidateTokenExchangeRequest(tokenRequest, exchanger.Storage()) - if err != nil { - RequestError(w, r, err) - return - } -} - -func ParseTokenExchangeRequest(w http.ResponseWriter, r *http.Request) (oidc.TokenRequest, error) { - return nil, errors.New("Unimplemented") //TODO: impl -} - -func ValidateTokenExchangeRequest(tokenReq oidc.TokenRequest, storage Storage) error { - return errors.New("Unimplemented") //TODO: impl -} diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index 338e39a..f7939b5 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -23,6 +23,7 @@ type jwtProfileVerifier struct { offset time.Duration } +//NewJWTProfileVerifier creates a oidc.Verifier for JWT Profile assertions (authorization grant and client authentication) func NewJWTProfileVerifier(storage Storage, issuer string, maxAgeIAT, offset time.Duration) JWTProfileVerifier { return &jwtProfileVerifier{ storage: storage, @@ -48,6 +49,9 @@ func (v *jwtProfileVerifier) Offset() time.Duration { return v.offset } +//VerifyJWTAssertion verifies the assertion string from JWT Profile (authorization grant and client authentication) +// +//checks audience, exp, iat, signature and that issuer and sub are the same func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerifier) (*oidc.JWTTokenRequest, error) { request := new(oidc.JWTTokenRequest) payload, err := oidc.ParseToken(assertion, request) @@ -85,6 +89,7 @@ type jwtProfileKeySet struct { userID string } +//VerifySignature implements oidc.KeySet by getting the public key from Storage implementation func (k *jwtProfileKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) { keyID := "" for _, sig := range jws.Signatures { From 540a7bd7be7237fb9cb703a0a1c5de36d7c54677 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Thu, 29 Apr 2021 12:43:21 +0200 Subject: [PATCH 043/502] improve Loopback check --- pkg/op/authrequest.go | 15 ++++++++------- pkg/op/authrequest_test.go | 33 +++++++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/pkg/op/authrequest.go b/pkg/op/authrequest.go index 765027f..8e6d8eb 100644 --- a/pkg/op/authrequest.go +++ b/pkg/op/authrequest.go @@ -3,6 +3,7 @@ package op import ( "context" "fmt" + "net" "net/http" "net/url" "strings" @@ -184,7 +185,7 @@ func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.Res //ValidateAuthReqRedirectURINative validates the passed redirect_uri and response_type to the registered uris and client type func validateAuthReqRedirectURINative(client Client, uri string, responseType oidc.ResponseType) error { - parsedURL, isLoopback := LoopbackOrLocalhost(uri) + parsedURL, isLoopback := HTTPLoopbackOrLocalhost(uri) isCustomSchema := !strings.HasPrefix(uri, "http://") if utils.Contains(client.RedirectURIs(), uri) { if isLoopback || isCustomSchema { @@ -196,7 +197,7 @@ func validateAuthReqRedirectURINative(client Client, uri string, responseType oi return ErrInvalidRequestRedirectURI("The requested redirect_uri is missing in the client configuration. If you have any questions, you may contact the administrator of the application.") } for _, uri := range client.RedirectURIs() { - redirectURI, ok := LoopbackOrLocalhost(uri) + redirectURI, ok := HTTPLoopbackOrLocalhost(uri) if ok && equalURI(parsedURL, redirectURI) { return nil } @@ -208,16 +209,16 @@ func equalURI(url1, url2 *url.URL) bool { return url1.Path == url2.Path && url1.RawQuery == url2.RawQuery } -func LoopbackOrLocalhost(rawurl string) (*url.URL, bool) { +func HTTPLoopbackOrLocalhost(rawurl string) (*url.URL, bool) { parsedURL, err := url.Parse(rawurl) if err != nil { return nil, false } + if parsedURL.Scheme != "http" { + return nil, false + } hostName := parsedURL.Hostname() - return parsedURL, parsedURL.Scheme == "http" && - hostName == "localhost" || - hostName == "127.0.0.1" || - hostName == "::1" + return parsedURL, hostName == "localhost" || net.ParseIP(hostName).IsLoopback() } //ValidateAuthReqResponseType validates the passed response_type to the registered response types diff --git a/pkg/op/authrequest_test.go b/pkg/op/authrequest_test.go index 8427079..9bec1e7 100644 --- a/pkg/op/authrequest_test.go +++ b/pkg/op/authrequest_test.go @@ -316,16 +316,16 @@ func TestValidateAuthReqRedirectURI(t *testing.T) { false, }, { - "code flow registered http not confidential (user agent) fails", + "code flow registered http not confidential (native) fails", args{"http://registered.com/callback", - mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, false), + mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeNative, nil, false), oidc.ResponseTypeCode}, true, }, { - "code flow registered http not confidential (native) fails", + "code flow registered http not confidential (user agent) fails", args{"http://registered.com/callback", - mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeNative, nil, false), + mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, false), oidc.ResponseTypeCode}, true, }, @@ -344,7 +344,7 @@ func TestValidateAuthReqRedirectURI(t *testing.T) { false, }, { - "code flow registered http localhost native ok", + "code flow registered http loopback v6 native ok", args{"http://[::1]:4200/callback", mock.NewClientWithConfig(t, []string{"http://[::1]/callback"}, op.ApplicationTypeNative, nil, false), oidc.ResponseTypeCode}, @@ -420,6 +420,13 @@ func TestValidateAuthReqRedirectURI(t *testing.T) { oidc.ResponseTypeIDToken}, false, }, + { + "implicit flow registered http localhost web fails", + args{"http://localhost:9999/callback", + mock.NewClientWithConfig(t, []string{"http://localhost:9999/callback"}, op.ApplicationTypeWeb, nil, false), + oidc.ResponseTypeIDToken}, + true, + }, { "implicit flow registered http localhost user agent fails", args{"http://localhost:9999/callback", @@ -581,10 +588,15 @@ func Test_LoopbackOrLocalhost(t *testing.T) { true, }, { - "v6 no port ok", + "v6 short no port ok", args{url: "http://[::1]/test"}, true, }, + { + "v6 long no port ok", + args{url: "http://[0:0:0:0:0:0:0:1]/test"}, + true, + }, { "locahost no port ok", args{url: "http://localhost/test"}, @@ -596,10 +608,15 @@ func Test_LoopbackOrLocalhost(t *testing.T) { true, }, { - "v6 with port ok", + "v6 short with port ok", args{url: "http://[::1]:4200/test"}, true, }, + { + "v6 long with port ok", + args{url: "http://[0:0:0:0:0:0:0:1]:4200/test"}, + true, + }, { "localhost with port ok", args{url: "http://localhost:4200/test"}, @@ -608,7 +625,7 @@ func Test_LoopbackOrLocalhost(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if _, got := op.LoopbackOrLocalhost(tt.args.url); got != tt.want { + if _, got := op.HTTPLoopbackOrLocalhost(tt.args.url); got != tt.want { t.Errorf("loopbackOrLocalhost() = %v, want %v", got, tt.want) } }) From be04244212e4d8802c5d41398f03511c9f51d41c Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Tue, 11 May 2021 10:26:25 +0200 Subject: [PATCH 044/502] amr and scopes --- pkg/op/auth_request.go | 2 +- pkg/op/token.go | 27 ++++++++++++--------------- pkg/op/token_code.go | 2 +- pkg/op/token_refresh.go | 39 ++++++++++++++++++++++++++++----------- 4 files changed, 42 insertions(+), 28 deletions(-) diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index dcb5e39..09c633f 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -263,7 +263,7 @@ func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthReques //AuthResponseToken creates the successful token(s) authentication response func AuthResponseToken(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer, client Client) { createAccessToken := authReq.GetResponseType() != oidc.ResponseTypeIDTokenOnly - resp, err := CreateTokenResponse(r.Context(), authReq, client, authorizer, createAccessToken, "") + resp, err := CreateTokenResponse(r.Context(), authReq, client, authorizer, createAccessToken, "", "") if err != nil { AuthRequestError(w, r, authReq, err, authorizer.Encoder()) return diff --git a/pkg/op/token.go b/pkg/op/token.go index 8592003..c18aa30 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -21,12 +21,12 @@ type TokenRequest interface { GetScopes() []string } -func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Client, creator TokenCreator, createAccessToken bool, code string) (*oidc.AccessTokenResponse, error) { - var accessToken, refreshToken string +func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Client, creator TokenCreator, createAccessToken bool, code, refreshToken string) (*oidc.AccessTokenResponse, error) { + var accessToken, newRefreshToken string var validity time.Duration if createAccessToken { var err error - accessToken, refreshToken, validity, err = CreateAccessToken(ctx, request, client.AccessTokenType(), creator, client) + accessToken, newRefreshToken, validity, err = CreateAccessToken(ctx, request, client.AccessTokenType(), creator, client, refreshToken) if err != nil { return nil, err } @@ -47,18 +47,18 @@ func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Cli return &oidc.AccessTokenResponse{ AccessToken: accessToken, IDToken: idToken, - RefreshToken: refreshToken, + RefreshToken: newRefreshToken, TokenType: oidc.BearerToken, ExpiresIn: exp, }, nil } -func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storage) (id, refreshToken string, exp time.Time, err error) { +func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storage, refreshToken string) (id, newRefreshToken string, exp time.Time, err error) { if needsRefreshToken(tokenRequest) { - id, exp, err = storage.CreateToken(ctx, tokenRequest) - return + return storage.CreateTokens(ctx, tokenRequest, refreshToken) } - return storage.CreateTokens(ctx, tokenRequest, "hodor") + id, exp, err = storage.CreateToken(ctx, tokenRequest) + return } func needsRefreshToken(tokenRequest TokenRequest) bool { @@ -72,8 +72,8 @@ func needsRefreshToken(tokenRequest TokenRequest) bool { } } -func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTokenType AccessTokenType, creator TokenCreator, client Client) (accessToken, refreshToken string, validity time.Duration, err error) { - id, refreshToken, exp, err := createTokens(ctx, tokenRequest, creator.Storage()) +func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTokenType AccessTokenType, creator TokenCreator, client Client, refreshToken string) (accessToken, newRefreshToken string, validity time.Duration, err error) { + id, newRefreshToken, exp, err := createTokens(ctx, tokenRequest, creator.Storage(), refreshToken) if err != nil { return "", "", 0, err } @@ -108,8 +108,7 @@ func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, ex } type IDTokenRequest interface { - //GetACR() string - //GetAMR() []string + GetAMR() []string GetAudience() []string GetAuthTime() time.Time GetClientID() string @@ -120,13 +119,11 @@ type IDTokenRequest interface { func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, validity time.Duration, accessToken, code string, storage Storage, signer Signer, client Client) (string, error) { exp := time.Now().UTC().Add(client.ClockSkew()).Add(validity) var acr, nonce string - var amr []string if authRequest, ok := request.(AuthRequest); ok { acr = authRequest.GetACR() - amr = authRequest.GetAMR() nonce = authRequest.GetNonce() } - claims := oidc.NewIDTokenClaims(issuer, request.GetSubject(), request.GetAudience(), exp, request.GetAuthTime(), nonce, acr, amr, request.GetClientID(), client.ClockSkew()) + claims := oidc.NewIDTokenClaims(issuer, request.GetSubject(), request.GetAudience(), exp, request.GetAuthTime(), nonce, acr, request.GetAMR(), request.GetClientID(), client.ClockSkew()) scopes := client.RestrictAdditionalIdTokenScopes()(request.GetScopes()) if accessToken != "" { atHash, err := oidc.ClaimHash(accessToken, signer.SignatureAlgorithm()) diff --git a/pkg/op/token_code.go b/pkg/op/token_code.go index 0f27104..953181d 100644 --- a/pkg/op/token_code.go +++ b/pkg/op/token_code.go @@ -25,7 +25,7 @@ func CodeExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { RequestError(w, r, err) return } - resp, err := CreateTokenResponse(r.Context(), authReq, client, exchanger, true, tokenReq.Code) + resp, err := CreateTokenResponse(r.Context(), authReq, client, exchanger, true, tokenReq.Code, "") if err != nil { RequestError(w, r, err) return diff --git a/pkg/op/token_refresh.go b/pkg/op/token_refresh.go index 5f3c3a5..7a8632a 100644 --- a/pkg/op/token_refresh.go +++ b/pkg/op/token_refresh.go @@ -11,15 +11,13 @@ import ( ) type RefreshTokenRequest interface { - //GetID() string - //GetACR() string - //GetAMR() []string + GetAMR() []string GetAudience() []string GetAuthTime() time.Time GetClientID() string GetScopes() []string GetSubject() string - //GetRefreshToken() string + SetCurrentScopes(scopes oidc.Scopes) } //RefreshTokenExchange handles the OAuth 2.0 refresh_token grant, including @@ -29,12 +27,12 @@ func RefreshTokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exch if err != nil { RequestError(w, r, err) } - authReq, client, err := ValidateRefreshTokenRequest(r.Context(), tokenReq, exchanger) + validatedRequest, client, err := ValidateRefreshTokenRequest(r.Context(), tokenReq, exchanger) if err != nil { RequestError(w, r, err) return } - resp, err := CreateTokenResponse(r.Context(), authReq, client, exchanger, true, "") + resp, err := CreateTokenResponse(r.Context(), validatedRequest, client, exchanger, true, "", tokenReq.RefreshToken) if err != nil { RequestError(w, r, err) return @@ -58,14 +56,33 @@ func ValidateRefreshTokenRequest(ctx context.Context, tokenReq *oidc.RefreshToke if tokenReq.RefreshToken == "" { return nil, nil, ErrInvalidRequest("code missing") } - authReq, client, err := AuthorizeRefreshClient(ctx, tokenReq, exchanger) + request, client, err := AuthorizeRefreshClient(ctx, tokenReq, exchanger) if err != nil { return nil, nil, err } - if client.GetID() != authReq.GetClientID() { + if client.GetID() != request.GetClientID() { return nil, nil, ErrInvalidRequest("invalid auth code") } - return authReq, client, nil + if err = ValidateRefreshTokenScopes(tokenReq.Scopes, request); err != nil { + return nil, nil, err + } + return request, client, nil +} + +//ValidateRefreshTokenScopes validates that requested scope is a subset of the original auth request scope +//it will set the requested scopes as current scopes onto RefreshTokenRequest +//if empty the original scopes will be used +func ValidateRefreshTokenScopes(requestedScopes oidc.Scopes, authRequest RefreshTokenRequest) error { + if len(requestedScopes) == 0 { + return nil + } + for _, scope := range requestedScopes { + if !utils.Contains(authRequest.GetScopes(), scope) { + return errors.New("invalid_scope") + } + } + authRequest.SetCurrentScopes(requestedScopes) + return nil } //AuthorizeCodeClient checks the authorization of the client and that the used method was the one previously registered. @@ -105,9 +122,9 @@ func AuthorizeRefreshClient(ctx context.Context, tokenReq *oidc.RefreshTokenRequ //RefreshTokenRequestByRefreshToken returns the RefreshTokenRequest (data representing the original auth request) //corresponding to the refresh_token from Storage or an error func RefreshTokenRequestByRefreshToken(ctx context.Context, storage Storage, refreshToken string) (RefreshTokenRequest, error) { - authReq, err := storage.RefreshTokenRequestByRefreshToken(ctx, refreshToken) + request, err := storage.RefreshTokenRequestByRefreshToken(ctx, refreshToken) if err != nil { return nil, ErrInvalidRequest("invalid refreshToken") } - return authReq, nil + return request, nil } From 2a11a1979e804571a0c01b49d8ce5615ee04e222 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Tue, 11 May 2021 10:48:11 +0200 Subject: [PATCH 045/502] rename storage methods and fix mocks --- example/internal/mock/storage.go | 12 ++- pkg/op/mock/authorizer.mock.go | 39 ++++---- pkg/op/mock/client.mock.go | 73 +++++++-------- pkg/op/mock/configuration.mock.go | 59 ++++++------ pkg/op/mock/signer.mock.go | 23 ++--- pkg/op/mock/storage.mock.go | 145 ++++++++++++++++++------------ pkg/op/storage.go | 6 +- pkg/op/token.go | 4 +- pkg/op/token_jwt_profile.go | 2 +- pkg/op/token_refresh.go | 2 +- 10 files changed, 204 insertions(+), 161 deletions(-) diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index 247459e..3d7bb63 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -95,6 +95,8 @@ func (a *AuthRequest) GetScopes() []string { } } +func (a *AuthRequest) SetCurrentScopes(scopes oidc.Scopes) {} + func (a *AuthRequest) GetState() string { return "" } @@ -151,15 +153,19 @@ func (s *AuthStorage) AuthRequestByID(_ context.Context, id string) (op.AuthRequ } return a, nil } -func (s *AuthStorage) CreateToken(_ context.Context, authReq op.TokenRequest) (string, time.Time, error) { +func (s *AuthStorage) CreateAccessToken(ctx context.Context, request op.TokenRequest) (string, time.Time, error) { return "id", time.Now().UTC().Add(5 * time.Minute), nil } -func (s *AuthStorage) AuthRequestByRefreshToken(_ context.Context, token string) (op.AuthRequest, error) { - if token != c { +func (s *AuthStorage) CreateAccessAndRefreshTokens(ctx context.Context, request op.TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) { + return "id", "refreshToken", time.Now().UTC().Add(5 * time.Minute), nil +} +func (s *AuthStorage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (op.RefreshTokenRequest, error) { + if refreshToken != c { return nil, errors.New("invalid token") } return a, nil } + func (s *AuthStorage) TerminateSession(_ context.Context, userID, clientID string) error { return nil } diff --git a/pkg/op/mock/authorizer.mock.go b/pkg/op/mock/authorizer.mock.go index 5da2437..69f6927 100644 --- a/pkg/op/mock/authorizer.mock.go +++ b/pkg/op/mock/authorizer.mock.go @@ -5,36 +5,37 @@ package mock import ( + reflect "reflect" + op "github.com/caos/oidc/pkg/op" utils "github.com/caos/oidc/pkg/utils" gomock "github.com/golang/mock/gomock" - reflect "reflect" ) -// MockAuthorizer is a mock of Authorizer interface +// MockAuthorizer is a mock of Authorizer interface. type MockAuthorizer struct { ctrl *gomock.Controller recorder *MockAuthorizerMockRecorder } -// MockAuthorizerMockRecorder is the mock recorder for MockAuthorizer +// MockAuthorizerMockRecorder is the mock recorder for MockAuthorizer. type MockAuthorizerMockRecorder struct { mock *MockAuthorizer } -// NewMockAuthorizer creates a new mock instance +// NewMockAuthorizer creates a new mock instance. func NewMockAuthorizer(ctrl *gomock.Controller) *MockAuthorizer { mock := &MockAuthorizer{ctrl: ctrl} mock.recorder = &MockAuthorizerMockRecorder{mock} return mock } -// EXPECT returns an object that allows the caller to indicate expected use +// EXPECT returns an object that allows the caller to indicate expected use. func (m *MockAuthorizer) EXPECT() *MockAuthorizerMockRecorder { return m.recorder } -// Crypto mocks base method +// Crypto mocks base method. func (m *MockAuthorizer) Crypto() op.Crypto { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Crypto") @@ -42,13 +43,13 @@ func (m *MockAuthorizer) Crypto() op.Crypto { return ret0 } -// Crypto indicates an expected call of Crypto +// Crypto indicates an expected call of Crypto. func (mr *MockAuthorizerMockRecorder) Crypto() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Crypto", reflect.TypeOf((*MockAuthorizer)(nil).Crypto)) } -// Decoder mocks base method +// Decoder mocks base method. func (m *MockAuthorizer) Decoder() utils.Decoder { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Decoder") @@ -56,13 +57,13 @@ func (m *MockAuthorizer) Decoder() utils.Decoder { return ret0 } -// Decoder indicates an expected call of Decoder +// Decoder indicates an expected call of Decoder. func (mr *MockAuthorizerMockRecorder) Decoder() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Decoder", reflect.TypeOf((*MockAuthorizer)(nil).Decoder)) } -// Encoder mocks base method +// Encoder mocks base method. func (m *MockAuthorizer) Encoder() utils.Encoder { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Encoder") @@ -70,13 +71,13 @@ func (m *MockAuthorizer) Encoder() utils.Encoder { return ret0 } -// Encoder indicates an expected call of Encoder +// Encoder indicates an expected call of Encoder. func (mr *MockAuthorizerMockRecorder) Encoder() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Encoder", reflect.TypeOf((*MockAuthorizer)(nil).Encoder)) } -// IDTokenHintVerifier mocks base method +// IDTokenHintVerifier mocks base method. func (m *MockAuthorizer) IDTokenHintVerifier() op.IDTokenHintVerifier { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "IDTokenHintVerifier") @@ -84,13 +85,13 @@ func (m *MockAuthorizer) IDTokenHintVerifier() op.IDTokenHintVerifier { return ret0 } -// IDTokenHintVerifier indicates an expected call of IDTokenHintVerifier +// IDTokenHintVerifier indicates an expected call of IDTokenHintVerifier. func (mr *MockAuthorizerMockRecorder) IDTokenHintVerifier() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenHintVerifier", reflect.TypeOf((*MockAuthorizer)(nil).IDTokenHintVerifier)) } -// Issuer mocks base method +// Issuer mocks base method. func (m *MockAuthorizer) Issuer() string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Issuer") @@ -98,13 +99,13 @@ func (m *MockAuthorizer) Issuer() string { return ret0 } -// Issuer indicates an expected call of Issuer +// Issuer indicates an expected call of Issuer. func (mr *MockAuthorizerMockRecorder) Issuer() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Issuer", reflect.TypeOf((*MockAuthorizer)(nil).Issuer)) } -// Signer mocks base method +// Signer mocks base method. func (m *MockAuthorizer) Signer() op.Signer { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Signer") @@ -112,13 +113,13 @@ func (m *MockAuthorizer) Signer() op.Signer { return ret0 } -// Signer indicates an expected call of Signer +// Signer indicates an expected call of Signer. func (mr *MockAuthorizerMockRecorder) Signer() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Signer", reflect.TypeOf((*MockAuthorizer)(nil).Signer)) } -// Storage mocks base method +// Storage mocks base method. func (m *MockAuthorizer) Storage() op.Storage { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Storage") @@ -126,7 +127,7 @@ func (m *MockAuthorizer) Storage() op.Storage { return ret0 } -// Storage indicates an expected call of Storage +// Storage indicates an expected call of Storage. func (mr *MockAuthorizerMockRecorder) Storage() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Storage", reflect.TypeOf((*MockAuthorizer)(nil).Storage)) diff --git a/pkg/op/mock/client.mock.go b/pkg/op/mock/client.mock.go index 9d5fe41..e03ae0c 100644 --- a/pkg/op/mock/client.mock.go +++ b/pkg/op/mock/client.mock.go @@ -5,37 +5,38 @@ package mock import ( + reflect "reflect" + time "time" + oidc "github.com/caos/oidc/pkg/oidc" op "github.com/caos/oidc/pkg/op" gomock "github.com/golang/mock/gomock" - reflect "reflect" - time "time" ) -// MockClient is a mock of Client interface +// MockClient is a mock of Client interface. type MockClient struct { ctrl *gomock.Controller recorder *MockClientMockRecorder } -// MockClientMockRecorder is the mock recorder for MockClient +// MockClientMockRecorder is the mock recorder for MockClient. type MockClientMockRecorder struct { mock *MockClient } -// NewMockClient creates a new mock instance +// NewMockClient creates a new mock instance. func NewMockClient(ctrl *gomock.Controller) *MockClient { mock := &MockClient{ctrl: ctrl} mock.recorder = &MockClientMockRecorder{mock} return mock } -// EXPECT returns an object that allows the caller to indicate expected use +// EXPECT returns an object that allows the caller to indicate expected use. func (m *MockClient) EXPECT() *MockClientMockRecorder { return m.recorder } -// AccessTokenType mocks base method +// AccessTokenType mocks base method. func (m *MockClient) AccessTokenType() op.AccessTokenType { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AccessTokenType") @@ -43,13 +44,13 @@ func (m *MockClient) AccessTokenType() op.AccessTokenType { return ret0 } -// AccessTokenType indicates an expected call of AccessTokenType +// AccessTokenType indicates an expected call of AccessTokenType. func (mr *MockClientMockRecorder) AccessTokenType() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccessTokenType", reflect.TypeOf((*MockClient)(nil).AccessTokenType)) } -// ApplicationType mocks base method +// ApplicationType mocks base method. func (m *MockClient) ApplicationType() op.ApplicationType { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ApplicationType") @@ -57,13 +58,13 @@ func (m *MockClient) ApplicationType() op.ApplicationType { return ret0 } -// ApplicationType indicates an expected call of ApplicationType +// ApplicationType indicates an expected call of ApplicationType. func (mr *MockClientMockRecorder) ApplicationType() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplicationType", reflect.TypeOf((*MockClient)(nil).ApplicationType)) } -// AuthMethod mocks base method +// AuthMethod mocks base method. func (m *MockClient) AuthMethod() oidc.AuthMethod { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AuthMethod") @@ -71,13 +72,13 @@ func (m *MockClient) AuthMethod() oidc.AuthMethod { return ret0 } -// AuthMethod indicates an expected call of AuthMethod +// AuthMethod indicates an expected call of AuthMethod. func (mr *MockClientMockRecorder) AuthMethod() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthMethod", reflect.TypeOf((*MockClient)(nil).AuthMethod)) } -// ClockSkew mocks base method +// ClockSkew mocks base method. func (m *MockClient) ClockSkew() time.Duration { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ClockSkew") @@ -85,13 +86,13 @@ func (m *MockClient) ClockSkew() time.Duration { return ret0 } -// ClockSkew indicates an expected call of ClockSkew +// ClockSkew indicates an expected call of ClockSkew. func (mr *MockClientMockRecorder) ClockSkew() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClockSkew", reflect.TypeOf((*MockClient)(nil).ClockSkew)) } -// DevMode mocks base method +// DevMode mocks base method. func (m *MockClient) DevMode() bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DevMode") @@ -99,13 +100,13 @@ func (m *MockClient) DevMode() bool { return ret0 } -// DevMode indicates an expected call of DevMode +// DevMode indicates an expected call of DevMode. func (mr *MockClientMockRecorder) DevMode() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DevMode", reflect.TypeOf((*MockClient)(nil).DevMode)) } -// GetID mocks base method +// GetID mocks base method. func (m *MockClient) GetID() string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetID") @@ -113,13 +114,13 @@ func (m *MockClient) GetID() string { return ret0 } -// GetID indicates an expected call of GetID +// GetID indicates an expected call of GetID. func (mr *MockClientMockRecorder) GetID() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetID", reflect.TypeOf((*MockClient)(nil).GetID)) } -// IDTokenLifetime mocks base method +// IDTokenLifetime mocks base method. func (m *MockClient) IDTokenLifetime() time.Duration { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "IDTokenLifetime") @@ -127,13 +128,13 @@ func (m *MockClient) IDTokenLifetime() time.Duration { return ret0 } -// IDTokenLifetime indicates an expected call of IDTokenLifetime +// IDTokenLifetime indicates an expected call of IDTokenLifetime. func (mr *MockClientMockRecorder) IDTokenLifetime() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenLifetime", reflect.TypeOf((*MockClient)(nil).IDTokenLifetime)) } -// IDTokenUserinfoClaimsAssertion mocks base method +// IDTokenUserinfoClaimsAssertion mocks base method. func (m *MockClient) IDTokenUserinfoClaimsAssertion() bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "IDTokenUserinfoClaimsAssertion") @@ -141,13 +142,13 @@ func (m *MockClient) IDTokenUserinfoClaimsAssertion() bool { return ret0 } -// IDTokenUserinfoClaimsAssertion indicates an expected call of IDTokenUserinfoClaimsAssertion +// IDTokenUserinfoClaimsAssertion indicates an expected call of IDTokenUserinfoClaimsAssertion. func (mr *MockClientMockRecorder) IDTokenUserinfoClaimsAssertion() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenUserinfoClaimsAssertion", reflect.TypeOf((*MockClient)(nil).IDTokenUserinfoClaimsAssertion)) } -// IsScopeAllowed mocks base method +// IsScopeAllowed mocks base method. func (m *MockClient) IsScopeAllowed(arg0 string) bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "IsScopeAllowed", arg0) @@ -155,13 +156,13 @@ func (m *MockClient) IsScopeAllowed(arg0 string) bool { return ret0 } -// IsScopeAllowed indicates an expected call of IsScopeAllowed +// IsScopeAllowed indicates an expected call of IsScopeAllowed. func (mr *MockClientMockRecorder) IsScopeAllowed(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsScopeAllowed", reflect.TypeOf((*MockClient)(nil).IsScopeAllowed), arg0) } -// LoginURL mocks base method +// LoginURL mocks base method. func (m *MockClient) LoginURL(arg0 string) string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "LoginURL", arg0) @@ -169,13 +170,13 @@ func (m *MockClient) LoginURL(arg0 string) string { return ret0 } -// LoginURL indicates an expected call of LoginURL +// LoginURL indicates an expected call of LoginURL. func (mr *MockClientMockRecorder) LoginURL(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoginURL", reflect.TypeOf((*MockClient)(nil).LoginURL), arg0) } -// PostLogoutRedirectURIs mocks base method +// PostLogoutRedirectURIs mocks base method. func (m *MockClient) PostLogoutRedirectURIs() []string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PostLogoutRedirectURIs") @@ -183,13 +184,13 @@ func (m *MockClient) PostLogoutRedirectURIs() []string { return ret0 } -// PostLogoutRedirectURIs indicates an expected call of PostLogoutRedirectURIs +// PostLogoutRedirectURIs indicates an expected call of PostLogoutRedirectURIs. func (mr *MockClientMockRecorder) PostLogoutRedirectURIs() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostLogoutRedirectURIs", reflect.TypeOf((*MockClient)(nil).PostLogoutRedirectURIs)) } -// RedirectURIs mocks base method +// RedirectURIs mocks base method. func (m *MockClient) RedirectURIs() []string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RedirectURIs") @@ -197,13 +198,13 @@ func (m *MockClient) RedirectURIs() []string { return ret0 } -// RedirectURIs indicates an expected call of RedirectURIs +// RedirectURIs indicates an expected call of RedirectURIs. func (mr *MockClientMockRecorder) RedirectURIs() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RedirectURIs", reflect.TypeOf((*MockClient)(nil).RedirectURIs)) } -// ResponseTypes mocks base method +// ResponseTypes mocks base method. func (m *MockClient) ResponseTypes() []oidc.ResponseType { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ResponseTypes") @@ -211,13 +212,13 @@ func (m *MockClient) ResponseTypes() []oidc.ResponseType { return ret0 } -// ResponseTypes indicates an expected call of ResponseTypes +// ResponseTypes indicates an expected call of ResponseTypes. func (mr *MockClientMockRecorder) ResponseTypes() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResponseTypes", reflect.TypeOf((*MockClient)(nil).ResponseTypes)) } -// RestrictAdditionalAccessTokenScopes mocks base method +// RestrictAdditionalAccessTokenScopes mocks base method. func (m *MockClient) RestrictAdditionalAccessTokenScopes() func([]string) []string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RestrictAdditionalAccessTokenScopes") @@ -225,13 +226,13 @@ func (m *MockClient) RestrictAdditionalAccessTokenScopes() func([]string) []stri return ret0 } -// RestrictAdditionalAccessTokenScopes indicates an expected call of RestrictAdditionalAccessTokenScopes +// RestrictAdditionalAccessTokenScopes indicates an expected call of RestrictAdditionalAccessTokenScopes. func (mr *MockClientMockRecorder) RestrictAdditionalAccessTokenScopes() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestrictAdditionalAccessTokenScopes", reflect.TypeOf((*MockClient)(nil).RestrictAdditionalAccessTokenScopes)) } -// RestrictAdditionalIdTokenScopes mocks base method +// RestrictAdditionalIdTokenScopes mocks base method. func (m *MockClient) RestrictAdditionalIdTokenScopes() func([]string) []string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RestrictAdditionalIdTokenScopes") @@ -239,7 +240,7 @@ func (m *MockClient) RestrictAdditionalIdTokenScopes() func([]string) []string { return ret0 } -// RestrictAdditionalIdTokenScopes indicates an expected call of RestrictAdditionalIdTokenScopes +// RestrictAdditionalIdTokenScopes indicates an expected call of RestrictAdditionalIdTokenScopes. func (mr *MockClientMockRecorder) RestrictAdditionalIdTokenScopes() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestrictAdditionalIdTokenScopes", reflect.TypeOf((*MockClient)(nil).RestrictAdditionalIdTokenScopes)) diff --git a/pkg/op/mock/configuration.mock.go b/pkg/op/mock/configuration.mock.go index 4f83f35..f9f297e 100644 --- a/pkg/op/mock/configuration.mock.go +++ b/pkg/op/mock/configuration.mock.go @@ -5,35 +5,36 @@ package mock import ( + reflect "reflect" + op "github.com/caos/oidc/pkg/op" gomock "github.com/golang/mock/gomock" - reflect "reflect" ) -// MockConfiguration is a mock of Configuration interface +// MockConfiguration is a mock of Configuration interface. type MockConfiguration struct { ctrl *gomock.Controller recorder *MockConfigurationMockRecorder } -// MockConfigurationMockRecorder is the mock recorder for MockConfiguration +// MockConfigurationMockRecorder is the mock recorder for MockConfiguration. type MockConfigurationMockRecorder struct { mock *MockConfiguration } -// NewMockConfiguration creates a new mock instance +// NewMockConfiguration creates a new mock instance. func NewMockConfiguration(ctrl *gomock.Controller) *MockConfiguration { mock := &MockConfiguration{ctrl: ctrl} mock.recorder = &MockConfigurationMockRecorder{mock} return mock } -// EXPECT returns an object that allows the caller to indicate expected use +// EXPECT returns an object that allows the caller to indicate expected use. func (m *MockConfiguration) EXPECT() *MockConfigurationMockRecorder { return m.recorder } -// AuthMethodPostSupported mocks base method +// AuthMethodPostSupported mocks base method. func (m *MockConfiguration) AuthMethodPostSupported() bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AuthMethodPostSupported") @@ -41,13 +42,13 @@ func (m *MockConfiguration) AuthMethodPostSupported() bool { return ret0 } -// AuthMethodPostSupported indicates an expected call of AuthMethodPostSupported +// AuthMethodPostSupported indicates an expected call of AuthMethodPostSupported. func (mr *MockConfigurationMockRecorder) AuthMethodPostSupported() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthMethodPostSupported", reflect.TypeOf((*MockConfiguration)(nil).AuthMethodPostSupported)) } -// AuthMethodPrivateKeyJWTSupported mocks base method +// AuthMethodPrivateKeyJWTSupported mocks base method. func (m *MockConfiguration) AuthMethodPrivateKeyJWTSupported() bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AuthMethodPrivateKeyJWTSupported") @@ -55,13 +56,13 @@ func (m *MockConfiguration) AuthMethodPrivateKeyJWTSupported() bool { return ret0 } -// AuthMethodPrivateKeyJWTSupported indicates an expected call of AuthMethodPrivateKeyJWTSupported +// AuthMethodPrivateKeyJWTSupported indicates an expected call of AuthMethodPrivateKeyJWTSupported. func (mr *MockConfigurationMockRecorder) AuthMethodPrivateKeyJWTSupported() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthMethodPrivateKeyJWTSupported", reflect.TypeOf((*MockConfiguration)(nil).AuthMethodPrivateKeyJWTSupported)) } -// AuthorizationEndpoint mocks base method +// AuthorizationEndpoint mocks base method. func (m *MockConfiguration) AuthorizationEndpoint() op.Endpoint { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AuthorizationEndpoint") @@ -69,13 +70,13 @@ func (m *MockConfiguration) AuthorizationEndpoint() op.Endpoint { return ret0 } -// AuthorizationEndpoint indicates an expected call of AuthorizationEndpoint +// AuthorizationEndpoint indicates an expected call of AuthorizationEndpoint. func (mr *MockConfigurationMockRecorder) AuthorizationEndpoint() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthorizationEndpoint", reflect.TypeOf((*MockConfiguration)(nil).AuthorizationEndpoint)) } -// CodeMethodS256Supported mocks base method +// CodeMethodS256Supported mocks base method. func (m *MockConfiguration) CodeMethodS256Supported() bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CodeMethodS256Supported") @@ -83,13 +84,13 @@ func (m *MockConfiguration) CodeMethodS256Supported() bool { return ret0 } -// CodeMethodS256Supported indicates an expected call of CodeMethodS256Supported +// CodeMethodS256Supported indicates an expected call of CodeMethodS256Supported. func (mr *MockConfigurationMockRecorder) CodeMethodS256Supported() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CodeMethodS256Supported", reflect.TypeOf((*MockConfiguration)(nil).CodeMethodS256Supported)) } -// EndSessionEndpoint mocks base method +// EndSessionEndpoint mocks base method. func (m *MockConfiguration) EndSessionEndpoint() op.Endpoint { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "EndSessionEndpoint") @@ -97,13 +98,13 @@ func (m *MockConfiguration) EndSessionEndpoint() op.Endpoint { return ret0 } -// EndSessionEndpoint indicates an expected call of EndSessionEndpoint +// EndSessionEndpoint indicates an expected call of EndSessionEndpoint. func (mr *MockConfigurationMockRecorder) EndSessionEndpoint() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EndSessionEndpoint", reflect.TypeOf((*MockConfiguration)(nil).EndSessionEndpoint)) } -// GrantTypeJWTAuthorizationSupported mocks base method +// GrantTypeJWTAuthorizationSupported mocks base method. func (m *MockConfiguration) GrantTypeJWTAuthorizationSupported() bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GrantTypeJWTAuthorizationSupported") @@ -111,13 +112,13 @@ func (m *MockConfiguration) GrantTypeJWTAuthorizationSupported() bool { return ret0 } -// GrantTypeJWTAuthorizationSupported indicates an expected call of GrantTypeJWTAuthorizationSupported +// GrantTypeJWTAuthorizationSupported indicates an expected call of GrantTypeJWTAuthorizationSupported. func (mr *MockConfigurationMockRecorder) GrantTypeJWTAuthorizationSupported() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeJWTAuthorizationSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeJWTAuthorizationSupported)) } -// GrantTypeTokenExchangeSupported mocks base method +// GrantTypeTokenExchangeSupported mocks base method. func (m *MockConfiguration) GrantTypeTokenExchangeSupported() bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GrantTypeTokenExchangeSupported") @@ -125,13 +126,13 @@ func (m *MockConfiguration) GrantTypeTokenExchangeSupported() bool { return ret0 } -// GrantTypeTokenExchangeSupported indicates an expected call of GrantTypeTokenExchangeSupported +// GrantTypeTokenExchangeSupported indicates an expected call of GrantTypeTokenExchangeSupported. func (mr *MockConfigurationMockRecorder) GrantTypeTokenExchangeSupported() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeTokenExchangeSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeTokenExchangeSupported)) } -// IntrospectionEndpoint mocks base method +// IntrospectionEndpoint mocks base method. func (m *MockConfiguration) IntrospectionEndpoint() op.Endpoint { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "IntrospectionEndpoint") @@ -139,13 +140,13 @@ func (m *MockConfiguration) IntrospectionEndpoint() op.Endpoint { return ret0 } -// IntrospectionEndpoint indicates an expected call of IntrospectionEndpoint +// IntrospectionEndpoint indicates an expected call of IntrospectionEndpoint. func (mr *MockConfigurationMockRecorder) IntrospectionEndpoint() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntrospectionEndpoint", reflect.TypeOf((*MockConfiguration)(nil).IntrospectionEndpoint)) } -// Issuer mocks base method +// Issuer mocks base method. func (m *MockConfiguration) Issuer() string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Issuer") @@ -153,13 +154,13 @@ func (m *MockConfiguration) Issuer() string { return ret0 } -// Issuer indicates an expected call of Issuer +// Issuer indicates an expected call of Issuer. func (mr *MockConfigurationMockRecorder) Issuer() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Issuer", reflect.TypeOf((*MockConfiguration)(nil).Issuer)) } -// KeysEndpoint mocks base method +// KeysEndpoint mocks base method. func (m *MockConfiguration) KeysEndpoint() op.Endpoint { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "KeysEndpoint") @@ -167,13 +168,13 @@ func (m *MockConfiguration) KeysEndpoint() op.Endpoint { return ret0 } -// KeysEndpoint indicates an expected call of KeysEndpoint +// KeysEndpoint indicates an expected call of KeysEndpoint. func (mr *MockConfigurationMockRecorder) KeysEndpoint() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeysEndpoint", reflect.TypeOf((*MockConfiguration)(nil).KeysEndpoint)) } -// TokenEndpoint mocks base method +// TokenEndpoint mocks base method. func (m *MockConfiguration) TokenEndpoint() op.Endpoint { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "TokenEndpoint") @@ -181,13 +182,13 @@ func (m *MockConfiguration) TokenEndpoint() op.Endpoint { return ret0 } -// TokenEndpoint indicates an expected call of TokenEndpoint +// TokenEndpoint indicates an expected call of TokenEndpoint. func (mr *MockConfigurationMockRecorder) TokenEndpoint() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TokenEndpoint", reflect.TypeOf((*MockConfiguration)(nil).TokenEndpoint)) } -// UserinfoEndpoint mocks base method +// UserinfoEndpoint mocks base method. func (m *MockConfiguration) UserinfoEndpoint() op.Endpoint { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UserinfoEndpoint") @@ -195,7 +196,7 @@ func (m *MockConfiguration) UserinfoEndpoint() op.Endpoint { return ret0 } -// UserinfoEndpoint indicates an expected call of UserinfoEndpoint +// UserinfoEndpoint indicates an expected call of UserinfoEndpoint. func (mr *MockConfigurationMockRecorder) UserinfoEndpoint() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserinfoEndpoint", reflect.TypeOf((*MockConfiguration)(nil).UserinfoEndpoint)) diff --git a/pkg/op/mock/signer.mock.go b/pkg/op/mock/signer.mock.go index b52f9d4..0564aa1 100644 --- a/pkg/op/mock/signer.mock.go +++ b/pkg/op/mock/signer.mock.go @@ -6,35 +6,36 @@ package mock import ( context "context" + reflect "reflect" + gomock "github.com/golang/mock/gomock" jose "gopkg.in/square/go-jose.v2" - reflect "reflect" ) -// MockSigner is a mock of Signer interface +// MockSigner is a mock of Signer interface. type MockSigner struct { ctrl *gomock.Controller recorder *MockSignerMockRecorder } -// MockSignerMockRecorder is the mock recorder for MockSigner +// MockSignerMockRecorder is the mock recorder for MockSigner. type MockSignerMockRecorder struct { mock *MockSigner } -// NewMockSigner creates a new mock instance +// NewMockSigner creates a new mock instance. func NewMockSigner(ctrl *gomock.Controller) *MockSigner { mock := &MockSigner{ctrl: ctrl} mock.recorder = &MockSignerMockRecorder{mock} return mock } -// EXPECT returns an object that allows the caller to indicate expected use +// EXPECT returns an object that allows the caller to indicate expected use. func (m *MockSigner) EXPECT() *MockSignerMockRecorder { return m.recorder } -// Health mocks base method +// Health mocks base method. func (m *MockSigner) Health(arg0 context.Context) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Health", arg0) @@ -42,13 +43,13 @@ func (m *MockSigner) Health(arg0 context.Context) error { return ret0 } -// Health indicates an expected call of Health +// Health indicates an expected call of Health. func (mr *MockSignerMockRecorder) Health(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockSigner)(nil).Health), arg0) } -// SignatureAlgorithm mocks base method +// SignatureAlgorithm mocks base method. func (m *MockSigner) SignatureAlgorithm() jose.SignatureAlgorithm { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SignatureAlgorithm") @@ -56,13 +57,13 @@ func (m *MockSigner) SignatureAlgorithm() jose.SignatureAlgorithm { return ret0 } -// SignatureAlgorithm indicates an expected call of SignatureAlgorithm +// SignatureAlgorithm indicates an expected call of SignatureAlgorithm. func (mr *MockSignerMockRecorder) SignatureAlgorithm() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignatureAlgorithm", reflect.TypeOf((*MockSigner)(nil).SignatureAlgorithm)) } -// Signer mocks base method +// Signer mocks base method. func (m *MockSigner) Signer() jose.Signer { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Signer") @@ -70,7 +71,7 @@ func (m *MockSigner) Signer() jose.Signer { return ret0 } -// Signer indicates an expected call of Signer +// Signer indicates an expected call of Signer. func (mr *MockSignerMockRecorder) Signer() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Signer", reflect.TypeOf((*MockSigner)(nil).Signer)) diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index 280e8e6..be261bb 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -6,38 +6,39 @@ package mock import ( context "context" + reflect "reflect" + time "time" + oidc "github.com/caos/oidc/pkg/oidc" op "github.com/caos/oidc/pkg/op" gomock "github.com/golang/mock/gomock" jose "gopkg.in/square/go-jose.v2" - reflect "reflect" - time "time" ) -// MockStorage is a mock of Storage interface +// MockStorage is a mock of Storage interface. type MockStorage struct { ctrl *gomock.Controller recorder *MockStorageMockRecorder } -// MockStorageMockRecorder is the mock recorder for MockStorage +// MockStorageMockRecorder is the mock recorder for MockStorage. type MockStorageMockRecorder struct { mock *MockStorage } -// NewMockStorage creates a new mock instance +// NewMockStorage creates a new mock instance. func NewMockStorage(ctrl *gomock.Controller) *MockStorage { mock := &MockStorage{ctrl: ctrl} mock.recorder = &MockStorageMockRecorder{mock} return mock } -// EXPECT returns an object that allows the caller to indicate expected use +// EXPECT returns an object that allows the caller to indicate expected use. func (m *MockStorage) EXPECT() *MockStorageMockRecorder { return m.recorder } -// AuthRequestByCode mocks base method +// AuthRequestByCode mocks base method. func (m *MockStorage) AuthRequestByCode(arg0 context.Context, arg1 string) (op.AuthRequest, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AuthRequestByCode", arg0, arg1) @@ -46,13 +47,13 @@ func (m *MockStorage) AuthRequestByCode(arg0 context.Context, arg1 string) (op.A return ret0, ret1 } -// AuthRequestByCode indicates an expected call of AuthRequestByCode +// AuthRequestByCode indicates an expected call of AuthRequestByCode. func (mr *MockStorageMockRecorder) AuthRequestByCode(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthRequestByCode", reflect.TypeOf((*MockStorage)(nil).AuthRequestByCode), arg0, arg1) } -// AuthRequestByID mocks base method +// AuthRequestByID mocks base method. func (m *MockStorage) AuthRequestByID(arg0 context.Context, arg1 string) (op.AuthRequest, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AuthRequestByID", arg0, arg1) @@ -61,13 +62,13 @@ func (m *MockStorage) AuthRequestByID(arg0 context.Context, arg1 string) (op.Aut return ret0, ret1 } -// AuthRequestByID indicates an expected call of AuthRequestByID +// AuthRequestByID indicates an expected call of AuthRequestByID. func (mr *MockStorageMockRecorder) AuthRequestByID(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthRequestByID", reflect.TypeOf((*MockStorage)(nil).AuthRequestByID), arg0, arg1) } -// AuthorizeClientIDSecret mocks base method +// AuthorizeClientIDSecret mocks base method. func (m *MockStorage) AuthorizeClientIDSecret(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AuthorizeClientIDSecret", arg0, arg1, arg2) @@ -75,13 +76,46 @@ func (m *MockStorage) AuthorizeClientIDSecret(arg0 context.Context, arg1, arg2 s return ret0 } -// AuthorizeClientIDSecret indicates an expected call of AuthorizeClientIDSecret +// AuthorizeClientIDSecret indicates an expected call of AuthorizeClientIDSecret. func (mr *MockStorageMockRecorder) AuthorizeClientIDSecret(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthorizeClientIDSecret", reflect.TypeOf((*MockStorage)(nil).AuthorizeClientIDSecret), arg0, arg1, arg2) } -// CreateAuthRequest mocks base method +// CreateAccessAndRefreshTokens mocks base method. +func (m *MockStorage) CreateAccessAndRefreshTokens(arg0 context.Context, arg1 op.TokenRequest, arg2 string) (string, string, time.Time, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateAccessAndRefreshTokens", arg0, arg1, arg2) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(time.Time) + ret3, _ := ret[3].(error) + return ret0, ret1, ret2, ret3 +} + +// CreateAccessAndRefreshTokens indicates an expected call of CreateAccessAndRefreshTokens. +func (mr *MockStorageMockRecorder) CreateAccessAndRefreshTokens(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAccessAndRefreshTokens", reflect.TypeOf((*MockStorage)(nil).CreateAccessAndRefreshTokens), arg0, arg1, arg2) +} + +// CreateAccessToken mocks base method. +func (m *MockStorage) CreateAccessToken(arg0 context.Context, arg1 op.TokenRequest) (string, time.Time, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateAccessToken", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(time.Time) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// CreateAccessToken indicates an expected call of CreateAccessToken. +func (mr *MockStorageMockRecorder) CreateAccessToken(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAccessToken", reflect.TypeOf((*MockStorage)(nil).CreateAccessToken), arg0, arg1) +} + +// CreateAuthRequest mocks base method. func (m *MockStorage) CreateAuthRequest(arg0 context.Context, arg1 *oidc.AuthRequest, arg2 string) (op.AuthRequest, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateAuthRequest", arg0, arg1, arg2) @@ -90,29 +124,13 @@ func (m *MockStorage) CreateAuthRequest(arg0 context.Context, arg1 *oidc.AuthReq return ret0, ret1 } -// CreateAuthRequest indicates an expected call of CreateAuthRequest +// CreateAuthRequest indicates an expected call of CreateAuthRequest. func (mr *MockStorageMockRecorder) CreateAuthRequest(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAuthRequest", reflect.TypeOf((*MockStorage)(nil).CreateAuthRequest), arg0, arg1, arg2) } -// CreateToken mocks base method -func (m *MockStorage) CreateToken(arg0 context.Context, arg1 op.TokenRequest) (string, time.Time, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateToken", arg0, arg1) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(time.Time) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// CreateToken indicates an expected call of CreateToken -func (mr *MockStorageMockRecorder) CreateToken(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateToken", reflect.TypeOf((*MockStorage)(nil).CreateToken), arg0, arg1) -} - -// DeleteAuthRequest mocks base method +// DeleteAuthRequest mocks base method. func (m *MockStorage) DeleteAuthRequest(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteAuthRequest", arg0, arg1) @@ -120,13 +138,13 @@ func (m *MockStorage) DeleteAuthRequest(arg0 context.Context, arg1 string) error return ret0 } -// DeleteAuthRequest indicates an expected call of DeleteAuthRequest +// DeleteAuthRequest indicates an expected call of DeleteAuthRequest. func (mr *MockStorageMockRecorder) DeleteAuthRequest(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAuthRequest", reflect.TypeOf((*MockStorage)(nil).DeleteAuthRequest), arg0, arg1) } -// GetClientByClientID mocks base method +// GetClientByClientID mocks base method. func (m *MockStorage) GetClientByClientID(arg0 context.Context, arg1 string) (op.Client, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetClientByClientID", arg0, arg1) @@ -135,13 +153,13 @@ func (m *MockStorage) GetClientByClientID(arg0 context.Context, arg1 string) (op return ret0, ret1 } -// GetClientByClientID indicates an expected call of GetClientByClientID +// GetClientByClientID indicates an expected call of GetClientByClientID. func (mr *MockStorageMockRecorder) GetClientByClientID(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClientByClientID", reflect.TypeOf((*MockStorage)(nil).GetClientByClientID), arg0, arg1) } -// GetKeyByIDAndUserID mocks base method +// GetKeyByIDAndUserID mocks base method. func (m *MockStorage) GetKeyByIDAndUserID(arg0 context.Context, arg1, arg2 string) (*jose.JSONWebKey, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetKeyByIDAndUserID", arg0, arg1, arg2) @@ -150,13 +168,13 @@ func (m *MockStorage) GetKeyByIDAndUserID(arg0 context.Context, arg1, arg2 strin return ret0, ret1 } -// GetKeyByIDAndUserID indicates an expected call of GetKeyByIDAndUserID +// GetKeyByIDAndUserID indicates an expected call of GetKeyByIDAndUserID. func (mr *MockStorageMockRecorder) GetKeyByIDAndUserID(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeyByIDAndUserID", reflect.TypeOf((*MockStorage)(nil).GetKeyByIDAndUserID), arg0, arg1, arg2) } -// GetKeySet mocks base method +// GetKeySet mocks base method. func (m *MockStorage) GetKeySet(arg0 context.Context) (*jose.JSONWebKeySet, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetKeySet", arg0) @@ -165,13 +183,13 @@ func (m *MockStorage) GetKeySet(arg0 context.Context) (*jose.JSONWebKeySet, erro return ret0, ret1 } -// GetKeySet indicates an expected call of GetKeySet +// GetKeySet indicates an expected call of GetKeySet. func (mr *MockStorageMockRecorder) GetKeySet(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeySet", reflect.TypeOf((*MockStorage)(nil).GetKeySet), arg0) } -// GetPrivateClaimsFromScopes mocks base method +// GetPrivateClaimsFromScopes mocks base method. func (m *MockStorage) GetPrivateClaimsFromScopes(arg0 context.Context, arg1, arg2 string, arg3 []string) (map[string]interface{}, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetPrivateClaimsFromScopes", arg0, arg1, arg2, arg3) @@ -180,25 +198,25 @@ func (m *MockStorage) GetPrivateClaimsFromScopes(arg0 context.Context, arg1, arg return ret0, ret1 } -// GetPrivateClaimsFromScopes indicates an expected call of GetPrivateClaimsFromScopes +// GetPrivateClaimsFromScopes indicates an expected call of GetPrivateClaimsFromScopes. func (mr *MockStorageMockRecorder) GetPrivateClaimsFromScopes(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrivateClaimsFromScopes", reflect.TypeOf((*MockStorage)(nil).GetPrivateClaimsFromScopes), arg0, arg1, arg2, arg3) } -// GetSigningKey mocks base method +// GetSigningKey mocks base method. func (m *MockStorage) GetSigningKey(arg0 context.Context, arg1 chan<- jose.SigningKey) { m.ctrl.T.Helper() m.ctrl.Call(m, "GetSigningKey", arg0, arg1) } -// GetSigningKey indicates an expected call of GetSigningKey +// GetSigningKey indicates an expected call of GetSigningKey. func (mr *MockStorageMockRecorder) GetSigningKey(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSigningKey", reflect.TypeOf((*MockStorage)(nil).GetSigningKey), arg0, arg1) } -// Health mocks base method +// Health mocks base method. func (m *MockStorage) Health(arg0 context.Context) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Health", arg0) @@ -206,13 +224,13 @@ func (m *MockStorage) Health(arg0 context.Context) error { return ret0 } -// Health indicates an expected call of Health +// Health indicates an expected call of Health. func (mr *MockStorageMockRecorder) Health(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockStorage)(nil).Health), arg0) } -// SaveAuthCode mocks base method +// SaveAuthCode mocks base method. func (m *MockStorage) SaveAuthCode(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SaveAuthCode", arg0, arg1, arg2) @@ -220,13 +238,13 @@ func (m *MockStorage) SaveAuthCode(arg0 context.Context, arg1, arg2 string) erro return ret0 } -// SaveAuthCode indicates an expected call of SaveAuthCode +// SaveAuthCode indicates an expected call of SaveAuthCode. func (mr *MockStorageMockRecorder) SaveAuthCode(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAuthCode", reflect.TypeOf((*MockStorage)(nil).SaveAuthCode), arg0, arg1, arg2) } -// SetIntrospectionFromToken mocks base method +// SetIntrospectionFromToken mocks base method. func (m *MockStorage) SetIntrospectionFromToken(arg0 context.Context, arg1 oidc.IntrospectionResponse, arg2, arg3, arg4 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetIntrospectionFromToken", arg0, arg1, arg2, arg3, arg4) @@ -234,13 +252,13 @@ func (m *MockStorage) SetIntrospectionFromToken(arg0 context.Context, arg1 oidc. return ret0 } -// SetIntrospectionFromToken indicates an expected call of SetIntrospectionFromToken +// SetIntrospectionFromToken indicates an expected call of SetIntrospectionFromToken. func (mr *MockStorageMockRecorder) SetIntrospectionFromToken(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIntrospectionFromToken", reflect.TypeOf((*MockStorage)(nil).SetIntrospectionFromToken), arg0, arg1, arg2, arg3, arg4) } -// SetUserinfoFromScopes mocks base method +// SetUserinfoFromScopes mocks base method. func (m *MockStorage) SetUserinfoFromScopes(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3 string, arg4 []string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetUserinfoFromScopes", arg0, arg1, arg2, arg3, arg4) @@ -248,13 +266,13 @@ func (m *MockStorage) SetUserinfoFromScopes(arg0 context.Context, arg1 oidc.User return ret0 } -// SetUserinfoFromScopes indicates an expected call of SetUserinfoFromScopes +// SetUserinfoFromScopes indicates an expected call of SetUserinfoFromScopes. func (mr *MockStorageMockRecorder) SetUserinfoFromScopes(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserinfoFromScopes", reflect.TypeOf((*MockStorage)(nil).SetUserinfoFromScopes), arg0, arg1, arg2, arg3, arg4) } -// SetUserinfoFromToken mocks base method +// SetUserinfoFromToken mocks base method. func (m *MockStorage) SetUserinfoFromToken(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3, arg4 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetUserinfoFromToken", arg0, arg1, arg2, arg3, arg4) @@ -262,13 +280,13 @@ func (m *MockStorage) SetUserinfoFromToken(arg0 context.Context, arg1 oidc.UserI return ret0 } -// SetUserinfoFromToken indicates an expected call of SetUserinfoFromToken +// SetUserinfoFromToken indicates an expected call of SetUserinfoFromToken. func (mr *MockStorageMockRecorder) SetUserinfoFromToken(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserinfoFromToken", reflect.TypeOf((*MockStorage)(nil).SetUserinfoFromToken), arg0, arg1, arg2, arg3, arg4) } -// TerminateSession mocks base method +// TerminateSession mocks base method. func (m *MockStorage) TerminateSession(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "TerminateSession", arg0, arg1, arg2) @@ -276,13 +294,28 @@ func (m *MockStorage) TerminateSession(arg0 context.Context, arg1, arg2 string) return ret0 } -// TerminateSession indicates an expected call of TerminateSession +// TerminateSession indicates an expected call of TerminateSession. func (mr *MockStorageMockRecorder) TerminateSession(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TerminateSession", reflect.TypeOf((*MockStorage)(nil).TerminateSession), arg0, arg1, arg2) } -// ValidateJWTProfileScopes mocks base method +// TokenRequestByRefreshToken mocks base method. +func (m *MockStorage) TokenRequestByRefreshToken(arg0 context.Context, arg1 string) (op.RefreshTokenRequest, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TokenRequestByRefreshToken", arg0, arg1) + ret0, _ := ret[0].(op.RefreshTokenRequest) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TokenRequestByRefreshToken indicates an expected call of TokenRequestByRefreshToken. +func (mr *MockStorageMockRecorder) TokenRequestByRefreshToken(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TokenRequestByRefreshToken", reflect.TypeOf((*MockStorage)(nil).TokenRequestByRefreshToken), arg0, arg1) +} + +// ValidateJWTProfileScopes mocks base method. func (m *MockStorage) ValidateJWTProfileScopes(arg0 context.Context, arg1 string, arg2 oidc.Scopes) (oidc.Scopes, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ValidateJWTProfileScopes", arg0, arg1, arg2) @@ -291,7 +324,7 @@ func (m *MockStorage) ValidateJWTProfileScopes(arg0 context.Context, arg1 string return ret0, ret1 } -// ValidateJWTProfileScopes indicates an expected call of ValidateJWTProfileScopes +// ValidateJWTProfileScopes indicates an expected call of ValidateJWTProfileScopes. func (mr *MockStorageMockRecorder) ValidateJWTProfileScopes(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateJWTProfileScopes", reflect.TypeOf((*MockStorage)(nil).ValidateJWTProfileScopes), arg0, arg1, arg2) diff --git a/pkg/op/storage.go b/pkg/op/storage.go index adba5cf..0e0794e 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -16,9 +16,9 @@ type AuthStorage interface { SaveAuthCode(context.Context, string, string) error DeleteAuthRequest(context.Context, string) error - CreateToken(context.Context, TokenRequest) (string, time.Time, error) - CreateTokens(ctx context.Context, request TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) - RefreshTokenRequestByRefreshToken(context.Context, string) (RefreshTokenRequest, error) + CreateAccessToken(context.Context, TokenRequest) (string, time.Time, error) + CreateAccessAndRefreshTokens(ctx context.Context, request TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) + TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (RefreshTokenRequest, error) TerminateSession(context.Context, string, string) error diff --git a/pkg/op/token.go b/pkg/op/token.go index c18aa30..28bc011 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -55,9 +55,9 @@ func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Cli func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storage, refreshToken string) (id, newRefreshToken string, exp time.Time, err error) { if needsRefreshToken(tokenRequest) { - return storage.CreateTokens(ctx, tokenRequest, refreshToken) + return storage.CreateAccessAndRefreshTokens(ctx, tokenRequest, refreshToken) } - id, exp, err = storage.CreateToken(ctx, tokenRequest) + id, exp, err = storage.CreateAccessToken(ctx, tokenRequest) return } diff --git a/pkg/op/token_jwt_profile.go b/pkg/op/token_jwt_profile.go index c96d4b2..ac3e2a1 100644 --- a/pkg/op/token_jwt_profile.go +++ b/pkg/op/token_jwt_profile.go @@ -55,7 +55,7 @@ func ParseJWTProfileGrantRequest(r *http.Request, decoder utils.Decoder) (*oidc. //CreateJWTTokenResponse creates func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator) (*oidc.AccessTokenResponse, error) { - id, exp, err := creator.Storage().CreateToken(ctx, tokenRequest) + id, exp, err := creator.Storage().CreateAccessToken(ctx, tokenRequest) if err != nil { return nil, err } diff --git a/pkg/op/token_refresh.go b/pkg/op/token_refresh.go index 7a8632a..6d61099 100644 --- a/pkg/op/token_refresh.go +++ b/pkg/op/token_refresh.go @@ -122,7 +122,7 @@ func AuthorizeRefreshClient(ctx context.Context, tokenReq *oidc.RefreshTokenRequ //RefreshTokenRequestByRefreshToken returns the RefreshTokenRequest (data representing the original auth request) //corresponding to the refresh_token from Storage or an error func RefreshTokenRequestByRefreshToken(ctx context.Context, storage Storage, refreshToken string) (RefreshTokenRequest, error) { - request, err := storage.RefreshTokenRequestByRefreshToken(ctx, refreshToken) + request, err := storage.TokenRequestByRefreshToken(ctx, refreshToken) if err != nil { return nil, ErrInvalidRequest("invalid refreshToken") } From 90b87289cb03bb88f57ce1e263663448d92e28b2 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Tue, 11 May 2021 15:17:10 +0200 Subject: [PATCH 046/502] Update pkg/op/token_code.go Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com> --- pkg/op/token_code.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/op/token_code.go b/pkg/op/token_code.go index 953181d..9aae67b 100644 --- a/pkg/op/token_code.go +++ b/pkg/op/token_code.go @@ -54,7 +54,7 @@ func ValidateAccessTokenRequest(ctx context.Context, tokenReq *oidc.AccessTokenR return nil, nil, ErrInvalidRequest("invalid auth code") } if tokenReq.RedirectURI != authReq.GetRedirectURI() { - return nil, nil, ErrInvalidRequest("redirect_uri does no correspond") + return nil, nil, ErrInvalidRequest("redirect_uri does not correspond") } return authReq, client, nil } From d362dd75463af1f5728db7370bee7eba996f2f20 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Tue, 11 May 2021 15:20:14 +0200 Subject: [PATCH 047/502] handle error --- pkg/op/token_refresh.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/op/token_refresh.go b/pkg/op/token_refresh.go index 6d61099..3cb1b24 100644 --- a/pkg/op/token_refresh.go +++ b/pkg/op/token_refresh.go @@ -114,7 +114,9 @@ func AuthorizeRefreshClient(ctx context.Context, tokenReq *oidc.RefreshTokenRequ if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() { return nil, nil, errors.New("auth_method post not supported") } - err = AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, exchanger.Storage()) + if err = AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, exchanger.Storage()); err != nil { + return nil, nil, err + } request, err = RefreshTokenRequestByRefreshToken(ctx, exchanger.Storage(), tokenReq.RefreshToken) return request, client, err } From 8e884bdb9fbdd6ee31632f97d02e41d8bd0dc0ee Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Tue, 18 May 2021 09:03:11 +0200 Subject: [PATCH 048/502] feat: refresh token (#98) add missing feature commit and readme update --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 41c5d85..34c8717 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,10 @@ TBD ## Features -| | Code Flow | Implicit Flow | Hybrid Flow | Discovery | PKCE | Token Exchange | mTLS | JWT Profile | -|----------------|-----------|---------------|-------------|-----------|------|----------------|---------|-------------| -| Relaying Party | yes | yes | not yet | yes | yes | partial | not yet | yes | -| Origin Party | yes | yes | not yet | yes | yes | not yet | not yet | yes | +| | Code Flow | Implicit Flow | Hybrid Flow | Discovery | PKCE | Token Exchange | mTLS | JWT Profile | Refresh Token | +|----------------|-----------|---------------|-------------|-----------|------|----------------|---------|-------------|---------------| +| Relaying Party | yes | yes | not yet | yes | yes | partial | not yet | yes | yes | +| Origin Party | yes | yes | not yet | yes | yes | not yet | not yet | yes | yes | ### Resources From 14faebbb779e8b5452eb2f2a136e843c9c8e129e Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Thu, 27 May 2021 13:44:11 +0200 Subject: [PATCH 049/502] fix: check grant types and add refresh token to discovery --- example/internal/mock/storage.go | 4 ++++ pkg/op/client.go | 1 + pkg/op/config.go | 1 + pkg/op/discovery.go | 3 +++ pkg/op/mock/client.mock.go | 14 ++++++++++++++ pkg/op/mock/configuration.mock.go | 14 ++++++++++++++ pkg/op/mock/storage.mock.impl.go | 4 ++++ pkg/op/op.go | 5 +++++ pkg/op/token.go | 10 +++++----- pkg/op/token_code.go | 3 +++ pkg/op/token_request.go | 20 ++++++++++++++++++-- 11 files changed, 72 insertions(+), 7 deletions(-) diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index 3d7bb63..b8a1648 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -251,6 +251,7 @@ type ConfClient struct { applicationType op.ApplicationType authMethod oidc.AuthMethod responseTypes []oidc.ResponseType + grantTypes []oidc.GrantType ID string accessTokenType op.AccessTokenType devMode bool @@ -295,6 +296,9 @@ func (c *ConfClient) AccessTokenType() op.AccessTokenType { func (c *ConfClient) ResponseTypes() []oidc.ResponseType { return c.responseTypes } +func (c *ConfClient) GrantTypes() []oidc.GrantType { + return c.grantTypes +} func (c *ConfClient) DevMode() bool { return c.devMode diff --git a/pkg/op/client.go b/pkg/op/client.go index 79715b0..f1e18fa 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -30,6 +30,7 @@ type Client interface { ApplicationType() ApplicationType AuthMethod() oidc.AuthMethod ResponseTypes() []oidc.ResponseType + GrantTypes() []oidc.GrantType LoginURL(string) string AccessTokenType() AccessTokenType IDTokenLifetime() time.Duration diff --git a/pkg/op/config.go b/pkg/op/config.go index 7cb522a..0e5216b 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -21,6 +21,7 @@ type Configuration interface { AuthMethodPostSupported() bool CodeMethodS256Supported() bool AuthMethodPrivateKeyJWTSupported() bool + GrantTypeRefreshTokenSupported() bool GrantTypeTokenExchangeSupported() bool GrantTypeJWTAuthorizationSupported() bool } diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index d8ef7c3..d057042 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -66,6 +66,9 @@ func GrantTypes(c Configuration) []oidc.GrantType { oidc.GrantTypeCode, oidc.GrantTypeImplicit, } + if c.GrantTypeRefreshTokenSupported() { + grantTypes = append(grantTypes, oidc.GrantTypeRefreshToken) + } if c.GrantTypeTokenExchangeSupported() { grantTypes = append(grantTypes, oidc.GrantTypeTokenExchange) } diff --git a/pkg/op/mock/client.mock.go b/pkg/op/mock/client.mock.go index e03ae0c..f78754d 100644 --- a/pkg/op/mock/client.mock.go +++ b/pkg/op/mock/client.mock.go @@ -120,6 +120,20 @@ func (mr *MockClientMockRecorder) GetID() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetID", reflect.TypeOf((*MockClient)(nil).GetID)) } +// GrantTypes mocks base method. +func (m *MockClient) GrantTypes() []oidc.GrantType { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GrantTypes") + ret0, _ := ret[0].([]oidc.GrantType) + return ret0 +} + +// GrantTypes indicates an expected call of GrantTypes. +func (mr *MockClientMockRecorder) GrantTypes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypes", reflect.TypeOf((*MockClient)(nil).GrantTypes)) +} + // IDTokenLifetime mocks base method. func (m *MockClient) IDTokenLifetime() time.Duration { m.ctrl.T.Helper() diff --git a/pkg/op/mock/configuration.mock.go b/pkg/op/mock/configuration.mock.go index f9f297e..da21751 100644 --- a/pkg/op/mock/configuration.mock.go +++ b/pkg/op/mock/configuration.mock.go @@ -118,6 +118,20 @@ func (mr *MockConfigurationMockRecorder) GrantTypeJWTAuthorizationSupported() *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeJWTAuthorizationSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeJWTAuthorizationSupported)) } +// GrantTypeRefreshTokenSupported mocks base method. +func (m *MockConfiguration) GrantTypeRefreshTokenSupported() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GrantTypeRefreshTokenSupported") + ret0, _ := ret[0].(bool) + return ret0 +} + +// GrantTypeRefreshTokenSupported indicates an expected call of GrantTypeRefreshTokenSupported. +func (mr *MockConfigurationMockRecorder) GrantTypeRefreshTokenSupported() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeRefreshTokenSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeRefreshTokenSupported)) +} + // GrantTypeTokenExchangeSupported mocks base method. func (m *MockConfiguration) GrantTypeTokenExchangeSupported() bool { m.ctrl.T.Helper() diff --git a/pkg/op/mock/storage.mock.impl.go b/pkg/op/mock/storage.mock.impl.go index da04fc9..7689bd3 100644 --- a/pkg/op/mock/storage.mock.impl.go +++ b/pkg/op/mock/storage.mock.impl.go @@ -107,6 +107,7 @@ type ConfClient struct { authMethod oidc.AuthMethod accessTokenType op.AccessTokenType responseTypes []oidc.ResponseType + grantTypes []oidc.GrantType devMode bool } @@ -150,6 +151,9 @@ func (c *ConfClient) AccessTokenType() op.AccessTokenType { func (c *ConfClient) ResponseTypes() []oidc.ResponseType { return c.responseTypes } +func (c *ConfClient) GrantTypes() []oidc.GrantType { + return c.grantTypes +} func (c *ConfClient) DevMode() bool { return c.devMode } diff --git a/pkg/op/op.go b/pkg/op/op.go index c91865d..03b053d 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -84,6 +84,7 @@ type Config struct { DefaultLogoutRedirectURI string CodeMethodS256 bool AuthMethodPrivateKeyJWT bool + GrantTypeRefreshToken bool } type endpoints struct { @@ -189,6 +190,10 @@ func (o *openidProvider) AuthMethodPrivateKeyJWTSupported() bool { return o.config.AuthMethodPrivateKeyJWT } +func (o *openidProvider) GrantTypeRefreshTokenSupported() bool { + return o.config.GrantTypeRefreshToken +} + func (o *openidProvider) GrantTypeTokenExchangeSupported() bool { return false } diff --git a/pkg/op/token.go b/pkg/op/token.go index 28bc011..1faffa8 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -53,18 +53,18 @@ func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Cli }, nil } -func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storage, refreshToken string) (id, newRefreshToken string, exp time.Time, err error) { - if needsRefreshToken(tokenRequest) { +func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storage, refreshToken string, client Client) (id, newRefreshToken string, exp time.Time, err error) { + if needsRefreshToken(tokenRequest, client) { return storage.CreateAccessAndRefreshTokens(ctx, tokenRequest, refreshToken) } id, exp, err = storage.CreateAccessToken(ctx, tokenRequest) return } -func needsRefreshToken(tokenRequest TokenRequest) bool { +func needsRefreshToken(tokenRequest TokenRequest, client Client) bool { switch req := tokenRequest.(type) { case AuthRequest: - return utils.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && req.GetResponseType() == oidc.ResponseTypeCode + return utils.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && req.GetResponseType() == oidc.ResponseTypeCode && ValidateGrantType(client, oidc.GrantTypeRefreshToken) case RefreshTokenRequest: return true default: @@ -73,7 +73,7 @@ func needsRefreshToken(tokenRequest TokenRequest) bool { } func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTokenType AccessTokenType, creator TokenCreator, client Client, refreshToken string) (accessToken, newRefreshToken string, validity time.Duration, err error) { - id, newRefreshToken, exp, err := createTokens(ctx, tokenRequest, creator.Storage(), refreshToken) + id, newRefreshToken, exp, err := createTokens(ctx, tokenRequest, creator.Storage(), refreshToken, client) if err != nil { return "", "", 0, err } diff --git a/pkg/op/token_code.go b/pkg/op/token_code.go index 9aae67b..fa941df 100644 --- a/pkg/op/token_code.go +++ b/pkg/op/token_code.go @@ -53,6 +53,9 @@ func ValidateAccessTokenRequest(ctx context.Context, tokenReq *oidc.AccessTokenR if client.GetID() != authReq.GetClientID() { return nil, nil, ErrInvalidRequest("invalid auth code") } + if !ValidateGrantType(client, oidc.GrantTypeCode) { + return nil, nil, ErrInvalidRequest("invalid_grant") + } if tokenReq.RedirectURI != authReq.GetRedirectURI() { return nil, nil, ErrInvalidRequest("redirect_uri does not correspond") } diff --git a/pkg/op/token_request.go b/pkg/op/token_request.go index f8148d7..fd26f19 100644 --- a/pkg/op/token_request.go +++ b/pkg/op/token_request.go @@ -17,6 +17,7 @@ type Exchanger interface { Crypto() Crypto AuthMethodPostSupported() bool AuthMethodPrivateKeyJWTSupported() bool + GrantTypeRefreshTokenSupported() bool GrantTypeTokenExchangeSupported() bool GrantTypeJWTAuthorizationSupported() bool } @@ -28,8 +29,10 @@ func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Reque CodeExchange(w, r, exchanger) return case string(oidc.GrantTypeRefreshToken): - RefreshTokenExchange(w, r, exchanger) - return + if exchanger.GrantTypeRefreshTokenSupported() { + RefreshTokenExchange(w, r, exchanger) + return + } case string(oidc.GrantTypeBearer): if ex, ok := exchanger.(JWTAuthorizationGrantExchanger); ok && exchanger.GrantTypeJWTAuthorizationSupported() { JWTProfile(w, r, ex) @@ -119,3 +122,16 @@ func AuthorizePrivateJWTKey(ctx context.Context, clientAssertion string, exchang } return client, nil } + +//ValidateGrantType ensures that the requested grant_type is allowed by the Client +func ValidateGrantType(client Client, grantType oidc.GrantType) bool { + if client == nil { + return false + } + for _, grant := range client.GrantTypes() { + if grantType == grant { + return true + } + } + return false +} From 3e336a407592ac0faa6a98d01754244eff5a355c Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 31 May 2021 11:35:03 +0200 Subject: [PATCH 050/502] fix: check refresh token grant type (#100) --- pkg/op/token_refresh.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/op/token_refresh.go b/pkg/op/token_refresh.go index 3cb1b24..8072f30 100644 --- a/pkg/op/token_refresh.go +++ b/pkg/op/token_refresh.go @@ -69,7 +69,7 @@ func ValidateRefreshTokenRequest(ctx context.Context, tokenReq *oidc.RefreshToke return request, client, nil } -//ValidateRefreshTokenScopes validates that requested scope is a subset of the original auth request scope +//ValidateRefreshTokenScopes validates that the requested scope is a subset of the original auth request scope //it will set the requested scopes as current scopes onto RefreshTokenRequest //if empty the original scopes will be used func ValidateRefreshTokenScopes(requestedScopes oidc.Scopes, authRequest RefreshTokenRequest) error { @@ -97,6 +97,9 @@ func AuthorizeRefreshClient(ctx context.Context, tokenReq *oidc.RefreshTokenRequ if err != nil { return nil, nil, err } + if !ValidateGrantType(client, oidc.GrantTypeRefreshToken) { + return nil, nil, ErrInvalidRequest("invalid_grant") + } request, err = RefreshTokenRequestByRefreshToken(ctx, exchanger.Storage(), tokenReq.RefreshToken) return request, client, err } @@ -104,6 +107,9 @@ func AuthorizeRefreshClient(ctx context.Context, tokenReq *oidc.RefreshTokenRequ if err != nil { return nil, nil, err } + if !ValidateGrantType(client, oidc.GrantTypeRefreshToken) { + return nil, nil, ErrInvalidRequest("invalid_grant") + } if client.AuthMethod() == oidc.AuthMethodPrivateKeyJWT { return nil, nil, errors.New("invalid_grant") } From a2583ad7722f34f440b3aade8ef440dfe34e5511 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Mon, 14 Jun 2021 15:59:51 +0200 Subject: [PATCH 051/502] docs: improve wording (#103) --- README.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 34c8717..f7b4365 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,17 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/caos/oidc)](https://goreportcard.com/report/github.com/caos/oidc) [![codecov](https://codecov.io/gh/caos/oidc/branch/master/graph/badge.svg)](https://codecov.io/gh/caos/oidc) -> This project is in alpha state. It can AND will continue breaking until version 1.0.0 is released +> This project is in beta state. It can AND will continue breaking until version 1.0.0 is released ## What Is It -This project is a easy to use client and server implementation for the `OIDC` (Open ID Connect) standard written for `Go`. +This project is a easy to use client and server implementation for the `OIDC` (Open ID Connect) standard written for `Go`. Whenever possible we tried to reuse / extend existing packages like `OAuth2 for Go`. ## How To Use It -TBD +Check the `/example` folder where example code for different scenarios is located. ## Features @@ -28,7 +28,7 @@ TBD ### Resources -For your convinience you can find the relevant standards linked below. +For your convenience you can find the relevant standards linked below. - [OpenID Connect Core 1.0 incorporating errata set 1](https://openid.net/specs/openid-connect-core-1_0.html) - [Proof Key for Code Exchange by OAuth Public Clients](https://tools.ietf.org/html/rfc7636) @@ -47,7 +47,22 @@ For your convinience you can find the relevant standards linked below. ## Why another library -As of 2020 there are not a lot of `OIDC` librarys in `Go` which can handle server and client implementations. CAOS is strongly commited to the general field of IAM (Identity and Access Management) and as such, we need solid frameworks to implement services. +As of 2020 there are not a lot of `OIDC` library's in `Go` which can handle server and client implementations. CAOS is strongly committed to the general field of IAM (Identity and Access Management) and as such, we need solid frameworks to implement services. + +### Goals + +- [Certify this library as RP](https://openid.net/certification/#RPs) +- [Certify this library as OP](https://openid.net/certification/#OPs) + +### Other Go OpenID Connect library's + +[https://github.com/coreos/go-oidc](https://github.com/coreos/go-oidc) + +The `go-oidc` does only support `RP` and is not feasible to use as `OP` that's why we could not rely on `go-oidc` + +[https://github.com/ory/fosite](https://github.com/ory/fosite) + +We did not choose `fosite` because it implements `OAuth 2.0` on its own and does not rely in the golang provided package. Nonetheless this is a great project. ## License From 2b58427192c3acc250bb03034d835bc2abf963fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jun 2021 16:01:58 +0200 Subject: [PATCH 052/502] chore(deps): bump gopkg.in/square/go-jose.v2 from 2.5.1 to 2.6.0 (#101) Bumps [gopkg.in/square/go-jose.v2](https://github.com/square/go-jose) from 2.5.1 to 2.6.0. - [Release notes](https://github.com/square/go-jose/releases) - [Commits](https://github.com/square/go-jose/compare/v2.5.1...v2.6.0) --- updated-dependencies: - dependency-name: gopkg.in/square/go-jose.v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 58 ++-------------------------------------------------------- 2 files changed, 3 insertions(+), 57 deletions(-) diff --git a/go.mod b/go.mod index b94275f..4a72315 100644 --- a/go.mod +++ b/go.mod @@ -19,5 +19,5 @@ require ( golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 golang.org/x/text v0.3.6 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect - gopkg.in/square/go-jose.v2 v2.5.1 + gopkg.in/square/go-jose.v2 v2.6.0 ) diff --git a/go.sum b/go.sum index bb48d24..d540e1d 100644 --- a/go.sum +++ b/go.sum @@ -12,70 +12,50 @@ cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bP cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/caos/logging v0.0.2 h1:ebg5C/HN0ludYR+WkvnFjwSExF4wvyiWPyWGcKMYsoo= github.com/caos/logging v0.0.2/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0= -github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f h1:WBZRG4aNOuI15bLRrCgN8fCq8E5Xuty6jGbmSNEvSsU= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4 h1:rEvIZUSZ3fx39WIi3JkQqQBitGwpELBIYWeBVh6wn+E= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= 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 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -101,7 +81,6 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -116,9 +95,7 @@ github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6C github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0 h1:pMen7vLs8nvgEYhywH3KDWJIJTeEr2ULsVWHWYHQyBs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -126,14 +103,11 @@ github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99 h1:Ak8CrdlwwXwAZxzS66vgPt4U8yUZX7JwLvVR58FN5jM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= @@ -144,21 +118,14 @@ github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlI github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -166,15 +133,12 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -182,13 +146,11 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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 h1:5tjfNdR2ki3yYQ842+eX2sQHeiwpKJ0RnHO4IYOc4V8= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 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= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -205,10 +167,8 @@ golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -219,17 +179,14 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -273,7 +230,6 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -313,7 +269,6 @@ golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/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= @@ -354,7 +309,6 @@ golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -376,7 +330,6 @@ google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -413,7 +366,6 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 h1:PDIOdWxZ8eRizhKa1AAvY53xsvLB1cWorMjslvY3VA8= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -426,7 +378,6 @@ google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 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 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 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= @@ -443,10 +394,9 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= -gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -458,11 +408,7 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= From 0591a0d1ef7d1500d8ed42e394e1c480a425716b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jun 2021 16:02:48 +0200 Subject: [PATCH 053/502] chore(deps): bump github.com/golang/mock from 1.5.0 to 1.6.0 (#104) Bumps [github.com/golang/mock](https://github.com/golang/mock) from 1.5.0 to 1.6.0. - [Release notes](https://github.com/golang/mock/releases) - [Changelog](https://github.com/golang/mock/blob/master/.goreleaser.yml) - [Commits](https://github.com/golang/mock/compare/v1.5.0...v1.6.0) --- updated-dependencies: - dependency-name: github.com/golang/mock dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 3 +-- go.sum | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 4a72315..e582c70 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.15 require ( github.com/caos/logging v0.0.2 - github.com/golang/mock v1.5.0 + github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.2 // indirect github.com/google/go-github/v31 v31.0.0 github.com/google/uuid v1.2.0 @@ -15,7 +15,6 @@ require ( github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.7.0 - golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 golang.org/x/text v0.3.6 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect diff --git a/go.sum b/go.sum index d540e1d..d08c4eb 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -147,6 +147,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ 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/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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= @@ -188,6 +189,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -214,8 +216,8 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA= -golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -231,6 +233,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -258,8 +261,12 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -310,6 +317,7 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 400f5c4de451372e910eeed3288a9f79b28eab8c Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Wed, 16 Jun 2021 08:34:01 +0200 Subject: [PATCH 054/502] fix: parse max_age and prompt correctly (and change scope type) (#105) * fix: parse max_age and prompt correctly (and change scope type) * remove unnecessary omitempty --- example/internal/mock/storage.go | 4 ++-- pkg/client/client.go | 4 ++-- pkg/client/rp/relaying_party.go | 4 ++-- pkg/oidc/authorization.go | 32 ++++++++++++------------- pkg/oidc/introspection.go | 12 +++++----- pkg/oidc/jwt_profile.go | 6 ++--- pkg/oidc/token_request.go | 40 ++++++++++++++++---------------- pkg/oidc/types.go | 24 ++++++++++++------- pkg/oidc/types_test.go | 12 +++++----- pkg/op/auth_request.go | 19 ++++++++++++++- pkg/op/auth_request_test.go | 2 +- pkg/op/mock/storage.mock.go | 4 ++-- pkg/op/mock/storage.mock.impl.go | 4 ++-- pkg/op/session.go | 10 -------- pkg/op/storage.go | 2 +- pkg/op/token_refresh.go | 4 ++-- 16 files changed, 98 insertions(+), 85 deletions(-) diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index b8a1648..214ba54 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -95,7 +95,7 @@ func (a *AuthRequest) GetScopes() []string { } } -func (a *AuthRequest) SetCurrentScopes(scopes oidc.Scopes) {} +func (a *AuthRequest) SetCurrentScopes(scopes []string) {} func (a *AuthRequest) GetState() string { return "" @@ -243,7 +243,7 @@ func (s *AuthStorage) SetIntrospectionFromToken(ctx context.Context, introspect return nil } -func (s *AuthStorage) ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) { +func (s *AuthStorage) ValidateJWTProfileScopes(ctx context.Context, userID string, scope []string) ([]string, error) { return scope, nil } diff --git a/pkg/client/client.go b/pkg/client/client.go index b2b815e..708b79c 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -17,8 +17,8 @@ import ( var ( Encoder = func() utils.Encoder { e := schema.NewEncoder() - e.RegisterEncoder(oidc.Scopes{}, func(value reflect.Value) string { - return value.Interface().(oidc.Scopes).Encode() + e.RegisterEncoder(oidc.SpaceDelimitedArray{}, func(value reflect.Value) string { + return value.Interface().(oidc.SpaceDelimitedArray).Encode() }) return e }() diff --git a/pkg/client/rp/relaying_party.go b/pkg/client/rp/relaying_party.go index 5a48951..94593ff 100644 --- a/pkg/client/rp/relaying_party.go +++ b/pkg/client/rp/relaying_party.go @@ -430,10 +430,10 @@ func WithCodeChallenge(codeChallenge string) AuthURLOpt { } //WithPrompt sets the `prompt` params in the auth request -func WithPrompt(prompt oidc.Prompt) AuthURLOpt { +func WithPrompt(prompt ...string) AuthURLOpt { return func() []oauth2.AuthCodeOption { return []oauth2.AuthCodeOption{ - oauth2.SetAuthURLParam("prompt", string(prompt)), + oauth2.SetAuthURLParam("prompt", oidc.SpaceDelimitedArray(prompt).Encode()), } } } diff --git a/pkg/oidc/authorization.go b/pkg/oidc/authorization.go index 71776af..79d0c1e 100644 --- a/pkg/oidc/authorization.go +++ b/pkg/oidc/authorization.go @@ -44,39 +44,39 @@ const ( //PromptNone (`none`) disallows the Authorization Server to display any authentication or consent user interface pages. //An error (login_required, interaction_required, ...) will be returned if the user is not already authenticated or consent is needed - PromptNone Prompt = "none" + PromptNone = "none" //PromptLogin (`login`) directs the Authorization Server to prompt the End-User for reauthentication. - PromptLogin Prompt = "login" + PromptLogin = "login" //PromptConsent (`consent`) directs the Authorization Server to prompt the End-User for consent (of sharing information). - PromptConsent Prompt = "consent" + PromptConsent = "consent" //PromptSelectAccount (`select_account `) directs the Authorization Server to prompt the End-User to select a user account (to enable multi user / session switching) - PromptSelectAccount Prompt = "select_account" + PromptSelectAccount = "select_account" ) //AuthRequest according to: //https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest type AuthRequest struct { ID string - Scopes Scopes `schema:"scope"` - ResponseType ResponseType `schema:"response_type"` - ClientID string `schema:"client_id"` - RedirectURI string `schema:"redirect_uri"` //TODO: type + Scopes SpaceDelimitedArray `schema:"scope"` + ResponseType ResponseType `schema:"response_type"` + ClientID string `schema:"client_id"` + RedirectURI string `schema:"redirect_uri"` //TODO: type State string `schema:"state"` // ResponseMode TODO: ? - Nonce string `schema:"nonce"` - Display Display `schema:"display"` - Prompt Prompt `schema:"prompt"` - MaxAge uint32 `schema:"max_age"` - UILocales Locales `schema:"ui_locales"` - IDTokenHint string `schema:"id_token_hint"` - LoginHint string `schema:"login_hint"` - ACRValues []string `schema:"acr_values"` + Nonce string `schema:"nonce"` + Display Display `schema:"display"` + Prompt SpaceDelimitedArray `schema:"prompt"` + MaxAge *uint `schema:"max_age"` + UILocales Locales `schema:"ui_locales"` + IDTokenHint string `schema:"id_token_hint"` + LoginHint string `schema:"login_hint"` + ACRValues []string `schema:"acr_values"` CodeChallenge string `schema:"code_challenge"` CodeChallengeMethod CodeChallengeMethod `schema:"code_challenge_method"` diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index a2176aa..9b2bad7 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -21,7 +21,7 @@ type IntrospectionResponse interface { UserInfoSetter SetActive(bool) IsActive() bool - SetScopes(scopes Scopes) + SetScopes(scopes []string) SetClientID(id string) } @@ -30,10 +30,10 @@ func NewIntrospectionResponse() IntrospectionResponse { } type introspectionResponse struct { - Active bool `json:"active"` - Scope Scopes `json:"scope,omitempty"` - ClientID string `json:"client_id,omitempty"` - Subject string `json:"sub,omitempty"` + Active bool `json:"active"` + Scope SpaceDelimitedArray `json:"scope,omitempty"` + ClientID string `json:"client_id,omitempty"` + Subject string `json:"sub,omitempty"` userInfoProfile userInfoEmail userInfoPhone @@ -46,7 +46,7 @@ func (u *introspectionResponse) IsActive() bool { return u.Active } -func (u *introspectionResponse) SetScopes(scope Scopes) { +func (u *introspectionResponse) SetScopes(scope []string) { u.Scope = scope } diff --git a/pkg/oidc/jwt_profile.go b/pkg/oidc/jwt_profile.go index 6969783..25b7caa 100644 --- a/pkg/oidc/jwt_profile.go +++ b/pkg/oidc/jwt_profile.go @@ -1,9 +1,9 @@ package oidc type JWTProfileGrantRequest struct { - Assertion string `schema:"assertion"` - Scope Scopes `schema:"scope"` - GrantType GrantType `schema:"grant_type"` + Assertion string `schema:"assertion"` + Scope SpaceDelimitedArray `schema:"scope"` + GrantType GrantType `schema:"grant_type"` } //NewJWTProfileGrantRequest creates an oauth2 `JSON Web Token (JWT) Profile` Grant diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index 1136f8e..4899c3a 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -58,12 +58,12 @@ func (a *AccessTokenRequest) SetClientSecret(clientSecret string) { } type RefreshTokenRequest struct { - RefreshToken string `schema:"refresh_token"` - Scopes Scopes `schema:"scope"` - ClientID string `schema:"client_id"` - ClientSecret string `schema:"client_secret"` - ClientAssertion string `schema:"client_assertion"` - ClientAssertionType string `schema:"client_assertion_type"` + RefreshToken string `schema:"refresh_token"` + Scopes SpaceDelimitedArray `schema:"scope"` + ClientID string `schema:"client_id"` + ClientSecret string `schema:"client_secret"` + ClientAssertion string `schema:"client_assertion"` + ClientAssertionType string `schema:"client_assertion_type"` } func (a *RefreshTokenRequest) GrantType() GrantType { @@ -81,12 +81,12 @@ func (a *RefreshTokenRequest) SetClientSecret(clientSecret string) { } type JWTTokenRequest struct { - Issuer string `json:"iss"` - Subject string `json:"sub"` - Scopes Scopes `json:"-"` - Audience Audience `json:"aud"` - IssuedAt Time `json:"iat"` - ExpiresAt Time `json:"exp"` + Issuer string `json:"iss"` + Subject string `json:"sub"` + Scopes SpaceDelimitedArray `json:"-"` + Audience Audience `json:"aud"` + IssuedAt Time `json:"iat"` + ExpiresAt Time `json:"exp"` } //GetIssuer implements the Claims interface @@ -143,12 +143,12 @@ func (j *JWTTokenRequest) GetScopes() []string { } type TokenExchangeRequest struct { - subjectToken string `schema:"subject_token"` - subjectTokenType string `schema:"subject_token_type"` - actorToken string `schema:"actor_token"` - actorTokenType string `schema:"actor_token_type"` - resource []string `schema:"resource"` - audience Audience `schema:"audience"` - Scope Scopes `schema:"scope"` - requestedTokenType string `schema:"requested_token_type"` + subjectToken string `schema:"subject_token"` + subjectTokenType string `schema:"subject_token_type"` + actorToken string `schema:"actor_token"` + actorTokenType string `schema:"actor_token_type"` + resource []string `schema:"resource"` + audience Audience `schema:"audience"` + Scope SpaceDelimitedArray `schema:"scope"` + requestedTokenType string `schema:"requested_token_type"` } diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index 5525923..e72d67c 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -54,30 +54,36 @@ func (l *Locales) UnmarshalText(text []byte) error { return nil } -type Prompt string +type MaxAge *uint + +func NewMaxAge(i uint) MaxAge { + return &i +} + +type SpaceDelimitedArray []string + +type Prompt SpaceDelimitedArray type ResponseType string -type Scopes []string - -func (s Scopes) Encode() string { +func (s SpaceDelimitedArray) Encode() string { return strings.Join(s, " ") } -func (s *Scopes) UnmarshalText(text []byte) error { +func (s *SpaceDelimitedArray) UnmarshalText(text []byte) error { *s = strings.Split(string(text), " ") return nil } -func (s *Scopes) MarshalText() ([]byte, error) { +func (s SpaceDelimitedArray) MarshalText() ([]byte, error) { return []byte(s.Encode()), nil } -func (s *Scopes) MarshalJSON() ([]byte, error) { - return json.Marshal((*s).Encode()) +func (s SpaceDelimitedArray) MarshalJSON() ([]byte, error) { + return json.Marshal((s).Encode()) } -func (s *Scopes) UnmarshalJSON(data []byte) error { +func (s *SpaceDelimitedArray) UnmarshalJSON(data []byte) error { var str string if err := json.Unmarshal(data, &str); err != nil { return err diff --git a/pkg/oidc/types_test.go b/pkg/oidc/types_test.go index 8138b4b..c03a775 100644 --- a/pkg/oidc/types_test.go +++ b/pkg/oidc/types_test.go @@ -220,7 +220,7 @@ func TestScopes_UnmarshalText(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var scopes Scopes + var scopes SpaceDelimitedArray if err := scopes.UnmarshalText(tt.args.text); (err != nil) != tt.wantErr { t.Errorf("UnmarshalText() error = %v, wantErr %v", err, tt.wantErr) } @@ -230,7 +230,7 @@ func TestScopes_UnmarshalText(t *testing.T) { } func TestScopes_MarshalText(t *testing.T) { type args struct { - scopes Scopes + scopes SpaceDelimitedArray } type res struct { scopes []byte @@ -244,7 +244,7 @@ func TestScopes_MarshalText(t *testing.T) { { "unknown value", args{ - Scopes{"unknown"}, + SpaceDelimitedArray{"unknown"}, }, res{ []byte("unknown"), @@ -254,7 +254,7 @@ func TestScopes_MarshalText(t *testing.T) { { "struct", args{ - Scopes{`{"unknown":"value"}`}, + SpaceDelimitedArray{`{"unknown":"value"}`}, }, res{ []byte(`{"unknown":"value"}`), @@ -264,7 +264,7 @@ func TestScopes_MarshalText(t *testing.T) { { "openid", args{ - Scopes{"openid"}, + SpaceDelimitedArray{"openid"}, }, res{ []byte("openid"), @@ -274,7 +274,7 @@ func TestScopes_MarshalText(t *testing.T) { { "multiple scopes", args{ - Scopes{"openid", "email", "custom:scope"}, + SpaceDelimitedArray{"openid", "email", "custom:scope"}, }, res{ []byte("openid email custom:scope"), diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 9e0cd45..fce681f 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -106,7 +106,11 @@ func ParseAuthorizeRequest(r *http.Request, decoder utils.Decoder) (*oidc.AuthRe } //ValidateAuthRequest validates the authorize parameters and returns the userID of the id_token_hint if passed -func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier IDTokenHintVerifier) (string, error) { +func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier IDTokenHintVerifier) (sub string, err error) { + authReq.MaxAge, err = ValidateAuthReqPrompt(authReq.Prompt, authReq.MaxAge) + if err != nil { + return "", err + } client, err := storage.GetClientByClientID(ctx, authReq.ClientID) if err != nil { return "", ErrServerError(err.Error()) @@ -124,6 +128,19 @@ func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage return ValidateAuthReqIDTokenHint(ctx, authReq.IDTokenHint, verifier) } +//ValidateAuthReqPrompt validates the passed prompt values and sets max_age to 0 if prompt login is present +func ValidateAuthReqPrompt(prompts []string, maxAge *uint) (_ *uint, err error) { + for _, prompt := range prompts { + if prompt == oidc.PromptNone && len(prompts) > 1 { + return nil, ErrInvalidRequest("The prompt parameter `none` must only be used as a single value") + } + if prompt == oidc.PromptLogin { + maxAge = oidc.NewMaxAge(0) + } + } + return maxAge, nil +} + //ValidateAuthReqScopes validates the passed scopes func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) { if len(scopes) == 0 { diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index 9bec1e7..40e1a8a 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -123,7 +123,7 @@ func TestParseAuthorizeRequest(t *testing.T) { }(), }, res{ - &oidc.AuthRequest{Scopes: oidc.Scopes{"openid"}}, + &oidc.AuthRequest{Scopes: oidc.SpaceDelimitedArray{"openid"}}, false, }, }, diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index be261bb..4b44f2b 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -316,10 +316,10 @@ func (mr *MockStorageMockRecorder) TokenRequestByRefreshToken(arg0, arg1 interfa } // ValidateJWTProfileScopes mocks base method. -func (m *MockStorage) ValidateJWTProfileScopes(arg0 context.Context, arg1 string, arg2 oidc.Scopes) (oidc.Scopes, error) { +func (m *MockStorage) ValidateJWTProfileScopes(arg0 context.Context, arg1 string, arg2 []string) ([]string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ValidateJWTProfileScopes", arg0, arg1, arg2) - ret0, _ := ret[0].(oidc.Scopes) + ret0, _ := ret[0].([]string) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/pkg/op/mock/storage.mock.impl.go b/pkg/op/mock/storage.mock.impl.go index 7689bd3..4855cf5 100644 --- a/pkg/op/mock/storage.mock.impl.go +++ b/pkg/op/mock/storage.mock.impl.go @@ -140,10 +140,10 @@ func (c *ConfClient) GetID() string { } func (c *ConfClient) AccessTokenLifetime() time.Duration { - return time.Duration(5 * time.Minute) + return 5 * time.Minute } func (c *ConfClient) IDTokenLifetime() time.Duration { - return time.Duration(5 * time.Minute) + return 5 * time.Minute } func (c *ConfClient) AccessTokenType() op.AccessTokenType { return c.accessTokenType diff --git a/pkg/op/session.go b/pkg/op/session.go index 19ebab4..4d75098 100644 --- a/pkg/op/session.go +++ b/pkg/op/session.go @@ -83,13 +83,3 @@ func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest, } return nil, ErrInvalidRequest("post_logout_redirect_uri invalid") } - -func NeedsExistingSession(authRequest *oidc.AuthRequest) bool { - if authRequest == nil { - return true - } - if authRequest.Prompt == oidc.PromptNone { - return true - } - return false -} diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 0e0794e..ca9ae7c 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -34,7 +34,7 @@ type OPStorage interface { SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, clientID string) error GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) - ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) + ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error) } type Storage interface { diff --git a/pkg/op/token_refresh.go b/pkg/op/token_refresh.go index 8072f30..debcca1 100644 --- a/pkg/op/token_refresh.go +++ b/pkg/op/token_refresh.go @@ -17,7 +17,7 @@ type RefreshTokenRequest interface { GetClientID() string GetScopes() []string GetSubject() string - SetCurrentScopes(scopes oidc.Scopes) + SetCurrentScopes(scopes []string) } //RefreshTokenExchange handles the OAuth 2.0 refresh_token grant, including @@ -72,7 +72,7 @@ func ValidateRefreshTokenRequest(ctx context.Context, tokenReq *oidc.RefreshToke //ValidateRefreshTokenScopes validates that the requested scope is a subset of the original auth request scope //it will set the requested scopes as current scopes onto RefreshTokenRequest //if empty the original scopes will be used -func ValidateRefreshTokenScopes(requestedScopes oidc.Scopes, authRequest RefreshTokenRequest) error { +func ValidateRefreshTokenScopes(requestedScopes []string, authRequest RefreshTokenRequest) error { if len(requestedScopes) == 0 { return nil } From 39fef3e7fb48ecebd28685d0a3ab6ed6e4d18c87 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 21 Jun 2021 14:04:38 +0200 Subject: [PATCH 055/502] fix: simplify JWTProfileVerifier interface --- pkg/op/verifier_jwt_profile.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index f7939b5..1dc5c41 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -13,18 +13,18 @@ import ( type JWTProfileVerifier interface { oidc.Verifier - Storage() Storage + Storage() jwtProfileKeyStorage } type jwtProfileVerifier struct { - storage Storage + storage jwtProfileKeyStorage issuer string maxAgeIAT time.Duration offset time.Duration } //NewJWTProfileVerifier creates a oidc.Verifier for JWT Profile assertions (authorization grant and client authentication) -func NewJWTProfileVerifier(storage Storage, issuer string, maxAgeIAT, offset time.Duration) JWTProfileVerifier { +func NewJWTProfileVerifier(storage jwtProfileKeyStorage, issuer string, maxAgeIAT, offset time.Duration) JWTProfileVerifier { return &jwtProfileVerifier{ storage: storage, issuer: issuer, @@ -37,7 +37,7 @@ func (v *jwtProfileVerifier) Issuer() string { return v.issuer } -func (v *jwtProfileVerifier) Storage() Storage { +func (v *jwtProfileVerifier) Storage() jwtProfileKeyStorage { return v.storage } @@ -84,9 +84,13 @@ func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerif return request, nil } +type jwtProfileKeyStorage interface { + GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) +} + type jwtProfileKeySet struct { - Storage - userID string + storage jwtProfileKeyStorage + userID string } //VerifySignature implements oidc.KeySet by getting the public key from Storage implementation @@ -96,7 +100,7 @@ func (k *jwtProfileKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWe keyID = sig.Header.KeyID break } - key, err := k.Storage.GetKeyByIDAndUserID(ctx, keyID, k.userID) + key, err := k.storage.GetKeyByIDAndUserID(ctx, keyID, k.userID) if err != nil { return nil, fmt.Errorf("error fetching keys: %w", err) } From 850faa159d8894f9db2ad2f8739266fc54cde4c5 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Wed, 23 Jun 2021 11:08:54 +0200 Subject: [PATCH 056/502] fix: rp verification process (#95) * fix: rp verification process * types * comments * fix cli client --- example/client/app/app.go | 27 +++++++++++---- pkg/client/client.go | 3 ++ pkg/client/rp/cli/cli.go | 2 +- pkg/client/rp/jwks.go | 56 ++++++++++++++++++++++--------- pkg/client/rp/relaying_party.go | 36 +++++++++++++++++--- pkg/client/rp/verifier.go | 6 +++- pkg/oidc/keyset.go | 58 ++++++++++++++++++++++++++++----- pkg/oidc/verifier.go | 16 +++++++-- pkg/op/op.go | 10 ++---- pkg/op/verifier_jwt_profile.go | 13 +++----- pkg/utils/hash.go | 3 ++ 11 files changed, 175 insertions(+), 55 deletions(-) diff --git a/example/client/app/app.go b/example/client/app/app.go index e3ddd15..d835959 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -59,11 +59,9 @@ func main() { //including state handling with secure cookie and the possibility to use PKCE http.Handle("/login", rp.AuthURLHandler(state, provider)) - //for demonstration purposes the returned tokens (access token, id_token an its parsed claims) - //are written as JSON objects onto response - marshal := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string) { - _ = state - data, err := json.Marshal(tokens) + //for demonstration purposes the returned userinfo response is written as JSON object onto response + marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) { + data, err := json.Marshal(info) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -71,10 +69,27 @@ func main() { w.Write(data) } + //you could also just take the access_token and id_token without calling the userinfo endpoint: + // + //marshalToken := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty) { + // data, err := json.Marshal(tokens) + // if err != nil { + // http.Error(w, err.Error(), http.StatusInternalServerError) + // return + // } + // w.Write(data) + //} + //register the CodeExchangeHandler at the callbackPath //the CodeExchangeHandler handles the auth response, creates the token request and calls the callback function //with the returned tokens from the token endpoint - http.Handle(callbackPath, rp.CodeExchangeHandler(marshal, provider)) + //in this example the callback function itself is wrapped by the UserinfoCallback which + //will call the Userinfo endpoint, check the sub and pass the info into the callback function + http.Handle(callbackPath, rp.CodeExchangeHandler(rp.UserinfoCallback(marshalUserinfo), provider)) + + //if you would use the callback without calling the userinfo endpoint, simply switch the callback handler for: + // + //http.Handle(callbackPath, rp.CodeExchangeHandler(marshalToken, provider)) lis := fmt.Sprintf("127.0.0.1:%s", port) logrus.Infof("listening on http://%s/", lis) diff --git a/pkg/client/client.go b/pkg/client/client.go index 708b79c..fa64b70 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -36,6 +36,9 @@ func Discover(issuer string, httpClient *http.Client) (*oidc.DiscoveryConfigurat if err != nil { return nil, err } + if discoveryConfig.Issuer != issuer { + return nil, oidc.ErrIssuerInvalid + } return discoveryConfig, nil } diff --git a/pkg/client/rp/cli/cli.go b/pkg/client/rp/cli/cli.go index a00f0bd..89566eb 100644 --- a/pkg/client/rp/cli/cli.go +++ b/pkg/client/rp/cli/cli.go @@ -19,7 +19,7 @@ func CodeFlow(ctx context.Context, relyingParty rp.RelyingParty, callbackPath, p tokenChan := make(chan *oidc.Tokens, 1) - callback := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string) { + callback := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty) { tokenChan <- tokens msg := "

Success!

" msg = msg + "

You are authenticated and can now return to the CLI.

" diff --git a/pkg/client/rp/jwks.go b/pkg/client/rp/jwks.go index 339fc93..98ed501 100644 --- a/pkg/client/rp/jwks.go +++ b/pkg/client/rp/jwks.go @@ -2,6 +2,7 @@ package rp import ( "context" + "encoding/json" "errors" "fmt" "net/http" @@ -21,6 +22,7 @@ func NewRemoteKeySet(client *http.Client, jwksURL string) oidc.KeySet { type remoteKeySet struct { jwksURL string httpClient *http.Client + defaultAlg string // guard all other fields mu sync.Mutex @@ -66,29 +68,27 @@ func (i *inflight) result() ([]jose.JSONWebKey, error) { } func (r *remoteKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { - // We don't support JWTs signed with multiple signatures. - keyID := "" - for _, sig := range jws.Signatures { - keyID = sig.Header.KeyID - break + keyID, alg := oidc.GetKeyIDAndAlg(jws) + if alg == "" { + alg = r.defaultAlg } - keys := r.keysFromCache() - payload, err, ok := oidc.CheckKey(keyID, jws, keys...) - if ok { + key, ok := oidc.FindKey(keyID, oidc.KeyUseSignature, alg, keys...) + if ok && keyID != "" { + payload, err := jws.Verify(&key) return payload, err } - keys, err = r.keysFromRemote(ctx) + keys, err := r.keysFromRemote(ctx) if err != nil { return nil, fmt.Errorf("fetching keys %v", err) } - - payload, err, ok = oidc.CheckKey(keyID, jws, keys...) - if !ok { - return nil, errors.New("invalid kid") + key, ok = oidc.FindKey(keyID, oidc.KeyUseSignature, alg, keys...) + if ok { + payload, err := jws.Verify(&key) + return payload, err } - return payload, err + return nil, errors.New("invalid key") } func (r *remoteKeySet) keysFromCache() (keys []jose.JSONWebKey) { @@ -147,10 +147,34 @@ func (r *remoteKeySet) fetchRemoteKeys(ctx context.Context) ([]jose.JSONWebKey, return nil, fmt.Errorf("oidc: can't create request: %v", err) } - keySet := new(jose.JSONWebKeySet) + keySet := new(jsonWebKeySet) if err = utils.HttpRequest(r.httpClient, req, keySet); err != nil { return nil, fmt.Errorf("oidc: failed to get keys: %v", err) } - return keySet.Keys, nil } + +//jsonWebKeySet is an alias for jose.JSONWebKeySet which ignores unknown key types (kty) +type jsonWebKeySet jose.JSONWebKeySet + +//UnmarshalJSON overrides the default jose.JSONWebKeySet method to ignore any error +//which might occur because of unknown key types (kty) +func (k *jsonWebKeySet) UnmarshalJSON(data []byte) (err error) { + var raw rawJSONWebKeySet + err = json.Unmarshal(data, &raw) + if err != nil { + return err + } + for _, key := range raw.Keys { + webKey := new(jose.JSONWebKey) + err = webKey.UnmarshalJSON(key) + if err == nil { + k.Keys = append(k.Keys, *webKey) + } + } + return nil +} + +type rawJSONWebKeySet struct { + Keys []json.RawMessage `json:"keys"` +} diff --git a/pkg/client/rp/relaying_party.go b/pkg/client/rp/relaying_party.go index 94593ff..c9c7a8c 100644 --- a/pkg/client/rp/relaying_party.go +++ b/pkg/client/rp/relaying_party.go @@ -22,6 +22,10 @@ const ( pkceCode = "pkce" ) +var ( + ErrUserInfoSubNotMatching = errors.New("sub from userinfo does not match the sub from the id_token") +) + //RelyingParty declares the minimal interface for oidc clients type RelyingParty interface { //OAuthConfig returns the oauth2 Config @@ -244,6 +248,9 @@ func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { if err != nil { return Endpoints{}, err } + if discoveryConfig.Issuer != issuer { + return Endpoints{}, oidc.ErrIssuerInvalid + } return GetEndpoints(discoveryConfig), nil } @@ -319,10 +326,12 @@ func CodeExchange(ctx context.Context, code string, rp RelyingParty, opts ...Cod return &oidc.Tokens{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil } +type CodeExchangeCallback func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp RelyingParty) + //CodeExchangeHandler extends the `CodeExchange` method with a http handler //including cookie handling for secure `state` transfer //and optional PKCE code verifier checking -func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string), rp RelyingParty) http.HandlerFunc { +func CodeExchangeHandler(callback CodeExchangeCallback, rp RelyingParty) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { state, err := tryReadStateCookie(w, r, rp) if err != nil { @@ -356,21 +365,40 @@ func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) return } - callback(w, r, tokens, state) + callback(w, r, tokens, state, rp) + } +} + +type CodeExchangeUserinfoCallback func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, provider RelyingParty, info oidc.UserInfo) + +//UserinfoCallback wraps the callback function of the CodeExchangeHandler +//and calls the userinfo endpoint with the access token +//on success it will pass the userinfo into its callback function as well +func UserinfoCallback(f CodeExchangeUserinfoCallback) CodeExchangeCallback { + return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp RelyingParty) { + info, err := Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp) + if err != nil { + http.Error(w, "userinfo failed: "+err.Error(), http.StatusUnauthorized) + return + } + f(w, r, tokens, state, rp, info) } } //Userinfo will call the OIDC Userinfo Endpoint with the provided token -func Userinfo(token string, rp RelyingParty) (oidc.UserInfo, error) { +func Userinfo(token, tokenType, subject string, rp RelyingParty) (oidc.UserInfo, error) { req, err := http.NewRequest("GET", rp.UserinfoEndpoint(), nil) if err != nil { return nil, err } - req.Header.Set("authorization", token) + req.Header.Set("authorization", tokenType+" "+token) userinfo := oidc.NewUserInfo() if err := utils.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil { return nil, err } + if userinfo.GetSubject() != subject { + return nil, ErrUserInfoSubNotMatching + } return userinfo, nil } diff --git a/pkg/client/rp/verifier.go b/pkg/client/rp/verifier.go index 1f45ca8..027ca79 100644 --- a/pkg/client/rp/verifier.go +++ b/pkg/client/rp/verifier.go @@ -46,7 +46,11 @@ func VerifyIDToken(ctx context.Context, token string, v IDTokenVerifier) (oidc.I return nil, err } - if err := oidc.CheckIssuer(claims, v.Issuer()); err != nil { + if err := oidc.CheckSubject(claims); err != nil { + return nil, err + } + + if err = oidc.CheckIssuer(claims, v.Issuer()); err != nil { return nil, err } diff --git a/pkg/oidc/keyset.go b/pkg/oidc/keyset.go index 0d8e02c..adfffcf 100644 --- a/pkg/oidc/keyset.go +++ b/pkg/oidc/keyset.go @@ -2,10 +2,17 @@ package oidc import ( "context" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" "gopkg.in/square/go-jose.v2" ) +const ( + KeyUseSignature = "sig" +) + //KeySet represents a set of JSON Web Keys // - remotely fetch via discovery and jwks_uri -> `remoteKeySet` // - held by the OP itself in storage -> `openIDKeySet` @@ -15,16 +22,51 @@ type KeySet interface { VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) } -//CheckKey searches the given JSON Web Keys for the requested key ID -//and verifies the JSON Web Signature with the found key +//GetKeyIDAndAlg returns the `kid` and `alg` claim from the JWS header +func GetKeyIDAndAlg(jws *jose.JSONWebSignature) (string, string) { + keyID := "" + alg := "" + for _, sig := range jws.Signatures { + keyID = sig.Header.KeyID + alg = sig.Header.Algorithm + break + } + return keyID, alg +} + +//FindKey searches the given JSON Web Keys for the requested key ID, usage and key type // -//will return false but no error if key ID is not found -func CheckKey(keyID string, jws *jose.JSONWebSignature, keys ...jose.JSONWebKey) ([]byte, error, bool) { +//will return the key immediately if matches exact (id, usage, type) +// +//will return false none or multiple match +func FindKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (jose.JSONWebKey, bool) { + var validKeys []jose.JSONWebKey for _, key := range keys { - if keyID == "" || key.KeyID == keyID { - payload, err := jws.Verify(&key) - return payload, err, true + if key.KeyID == keyID && key.Use == use && algToKeyType(key.Key, expectedAlg) { + if keyID != "" { + return key, true + } + validKeys = append(validKeys, key) } } - return nil, nil, false + if len(validKeys) == 1 { + return validKeys[0], true + } + return jose.JSONWebKey{}, false +} + +func algToKeyType(key interface{}, alg string) bool { + switch alg[0] { + case 'R', 'P': + _, ok := key.(*rsa.PublicKey) + return ok + case 'E': + _, ok := key.(*ecdsa.PublicKey) + return ok + case 'O': + _, ok := key.(*ed25519.PublicKey) + return ok + default: + return false + } } diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go index 06470a0..f8470b5 100644 --- a/pkg/oidc/verifier.go +++ b/pkg/oidc/verifier.go @@ -17,6 +17,7 @@ import ( type Claims interface { GetIssuer() string + GetSubject() string GetAudience() []string GetExpiration() time.Time GetIssuedAt() time.Time @@ -30,6 +31,7 @@ type Claims interface { var ( ErrParse = errors.New("parsing of request failed") ErrIssuerInvalid = errors.New("issuer does not match") + ErrSubjectMissing = errors.New("subject missing") ErrAudience = errors.New("audience is not valid") ErrAzpMissing = errors.New("authorized party is not set. If Token is valid for multiple audiences, azp must not be empty") ErrAzpInvalid = errors.New("authorized party is not valid") @@ -38,6 +40,7 @@ var ( ErrSignatureUnsupportedAlg = errors.New("signature algorithm not supported") ErrSignatureInvalidPayload = errors.New("signature does not match Payload") ErrExpired = errors.New("token has expired") + ErrIatMissing = errors.New("issuedAt of token is missing") ErrIatInFuture = errors.New("issuedAt of token is in the future") ErrIatToOld = errors.New("issuedAt of token is to old") ErrNonceInvalid = errors.New("nonce does not match") @@ -84,6 +87,13 @@ func ParseToken(tokenString string, claims interface{}) ([]byte, error) { return payload, err } +func CheckSubject(claims Claims) error { + if claims.GetSubject() == "" { + return ErrSubjectMissing + } + return nil +} + func CheckIssuer(claims Claims, issuer string) error { if claims.GetIssuer() != issuer { return fmt.Errorf("%w: Expected: %s, got: %s", ErrIssuerInvalid, issuer, claims.GetIssuer()) @@ -155,6 +165,9 @@ func CheckExpiration(claims Claims, offset time.Duration) error { func CheckIssuedAt(claims Claims, maxAgeIAT, offset time.Duration) error { issuedAt := claims.GetIssuedAt().Round(time.Second) + if issuedAt.IsZero() { + return ErrIatMissing + } nowWithOffset := time.Now().UTC().Add(offset).Round(time.Second) if issuedAt.After(nowWithOffset) { return fmt.Errorf("%w: (iat: %v, now with offset: %v)", ErrIatInFuture, issuedAt, nowWithOffset) @@ -170,9 +183,6 @@ func CheckIssuedAt(claims Claims, maxAgeIAT, offset time.Duration) error { } func CheckNonce(claims Claims, nonce string) error { - if nonce == "" { - return nil - } if claims.GetNonce() != nonce { return fmt.Errorf("%w: expected %q but was %q", ErrNonceInvalid, nonce, claims.GetNonce()) } diff --git a/pkg/op/op.go b/pkg/op/op.go index 03b053d..518ffdf 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -272,20 +272,16 @@ type openIDKeySet struct { //VerifySignature implements the oidc.KeySet interface //providing an implementation for the keys stored in the OP Storage interface func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { - keyID := "" - for _, sig := range jws.Signatures { - keyID = sig.Header.KeyID - break - } keySet, err := o.Storage.GetKeySet(ctx) if err != nil { return nil, errors.New("error fetching keys") } - payload, err, ok := oidc.CheckKey(keyID, jws, keySet.Keys...) + keyID, alg := oidc.GetKeyIDAndAlg(jws) + key, ok := oidc.FindKey(keyID, oidc.KeyUseSignature, alg, keySet.Keys...) if !ok { return nil, errors.New("invalid kid") } - return payload, err + return jws.Verify(key) } type Option func(o *openidProvider) error diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index f7939b5..b425f80 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -91,18 +91,13 @@ type jwtProfileKeySet struct { //VerifySignature implements oidc.KeySet by getting the public key from Storage implementation func (k *jwtProfileKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) { - keyID := "" - for _, sig := range jws.Signatures { - keyID = sig.Header.KeyID - break - } + keyID, alg := oidc.GetKeyIDAndAlg(jws) key, err := k.Storage.GetKeyByIDAndUserID(ctx, keyID, k.userID) if err != nil { return nil, fmt.Errorf("error fetching keys: %w", err) } - payload, err, ok := oidc.CheckKey(keyID, jws, *key) - if !ok { - return nil, errors.New("invalid kid") + if key.Algorithm != alg { + } - return payload, err + return jws.Verify(&key) } diff --git a/pkg/utils/hash.go b/pkg/utils/hash.go index b7dfd9c..5dae03c 100644 --- a/pkg/utils/hash.go +++ b/pkg/utils/hash.go @@ -24,6 +24,9 @@ func GetHashAlgorithm(sigAlgorithm jose.SignatureAlgorithm) (hash.Hash, error) { } func HashString(hash hash.Hash, s string, firstHalf bool) string { + if hash == nil { + return s + } //nolint:errcheck hash.Write([]byte(s)) size := hash.Size() From 0b446618c74bda09f1ce17abd72fa4d6ef2c41d9 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Wed, 23 Jun 2021 14:01:31 +0200 Subject: [PATCH 057/502] custom claims for assertion and jwt profile request --- pkg/oidc/token.go | 133 ++++++++++++++++++++++++++++++--- pkg/oidc/token_request.go | 46 ++++++++++++ pkg/op/verifier_jwt_profile.go | 53 +++++++++---- 3 files changed, 207 insertions(+), 25 deletions(-) diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index 068e8e6..44c0076 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -5,6 +5,7 @@ import ( "crypto/x509" "encoding/json" "encoding/pem" + "fmt" "io/ioutil" "time" @@ -399,7 +400,19 @@ type AccessTokenResponse struct { IDToken string `json:"id_token,omitempty" schema:"id_token,omitempty"` } -type JWTProfileAssertion struct { +type JWTProfileAssertionClaims interface { + GetKeyID() string + GetPrivateKey() []byte + GetIssuer() string + GetSubject() string + GetAudience() []string + GetExpiration() time.Time + GetIssuedAt() time.Time + SetCustomClaim(key string, value interface{}) + GetCustomClaim(key string) interface{} +} + +type jwtProfileAssertion struct { PrivateKeyID string `json:"-"` PrivateKey []byte `json:"-"` Issuer string `json:"iss"` @@ -407,17 +420,98 @@ type JWTProfileAssertion struct { Audience Audience `json:"aud"` Expiration Time `json:"exp"` IssuedAt Time `json:"iat"` + + customClaims map[string]interface{} } -func NewJWTProfileAssertionFromKeyJSON(filename string, audience []string) (*JWTProfileAssertion, error) { +func (j *jwtProfileAssertion) MarshalJSON() ([]byte, error) { + type Alias jwtProfileAssertion + a := (*Alias)(j) + a.Subject = "109050709344825901" + + b, err := json.Marshal(a) + if err != nil { + return nil, err + } + + if len(j.customClaims) == 0 { + return b, nil + } + + err = json.Unmarshal(b, &j.customClaims) + if err != nil { + return nil, fmt.Errorf("jws: invalid map of custom claims %v", j.customClaims) + } + + return json.Marshal(j.customClaims) +} + +func (j *jwtProfileAssertion) UnmarshalJSON(data []byte) error { + type Alias jwtProfileAssertion + a := (*Alias)(j) + a.Subject = "109050709344825901" + + err := json.Unmarshal(data, a) + if err != nil { + return err + } + + err = json.Unmarshal(data, &j.customClaims) + if err != nil { + return err + } + + return nil +} + +func (j *jwtProfileAssertion) GetKeyID() string { + return j.PrivateKeyID +} + +func (j *jwtProfileAssertion) GetPrivateKey() []byte { + return j.PrivateKey +} + +func (j *jwtProfileAssertion) SetCustomClaim(key string, value interface{}) { + if j.customClaims == nil { + j.customClaims = make(map[string]interface{}) + } + j.customClaims[key] = value +} + +func (j *jwtProfileAssertion) GetCustomClaim(key string) interface{} { + return j.customClaims[key] +} + +func (j *jwtProfileAssertion) GetIssuer() string { + return j.Issuer +} + +func (j *jwtProfileAssertion) GetSubject() string { + return j.Subject +} + +func (j *jwtProfileAssertion) GetAudience() []string { + return j.Audience +} + +func (j *jwtProfileAssertion) GetExpiration() time.Time { + return time.Time(j.Expiration) +} + +func (j *jwtProfileAssertion) GetIssuedAt() time.Time { + return time.Time(j.IssuedAt) +} + +func NewJWTProfileAssertionFromKeyJSON(filename string, audience []string, opts ...AssertionOption) (JWTProfileAssertionClaims, error) { data, err := ioutil.ReadFile(filename) if err != nil { return nil, err } - return NewJWTProfileAssertionFromFileData(data, audience) + return NewJWTProfileAssertionFromFileData(data, audience, opts...) } -func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string) (string, error) { +func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string, opts ...AssertionOption) (string, error) { keyData := new(struct { KeyID string `json:"keyId"` Key string `json:"key"` @@ -427,10 +521,16 @@ func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string) (s if err != nil { return "", err } - return GenerateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key))) + return GenerateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...)) } -func NewJWTProfileAssertionFromFileData(data []byte, audience []string) (*JWTProfileAssertion, error) { +func CustomClaim(key string, value interface{}) func(*jwtProfileAssertion) { + return func(j *jwtProfileAssertion) { + j.customClaims[key] = value + } +} + +func NewJWTProfileAssertionFromFileData(data []byte, audience []string, opts ...AssertionOption) (JWTProfileAssertionClaims, error) { keyData := new(struct { KeyID string `json:"keyId"` Key string `json:"key"` @@ -440,11 +540,13 @@ func NewJWTProfileAssertionFromFileData(data []byte, audience []string) (*JWTPro if err != nil { return nil, err } - return NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key)), nil + return NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...), nil } -func NewJWTProfileAssertion(userID, keyID string, audience []string, key []byte) *JWTProfileAssertion { - return &JWTProfileAssertion{ +type AssertionOption func(*jwtProfileAssertion) + +func NewJWTProfileAssertion(userID, keyID string, audience []string, key []byte, opts ...AssertionOption) JWTProfileAssertionClaims { + j := &jwtProfileAssertion{ PrivateKey: key, PrivateKeyID: keyID, Issuer: userID, @@ -452,7 +554,14 @@ func NewJWTProfileAssertion(userID, keyID string, audience []string, key []byte) IssuedAt: Time(time.Now().UTC()), Expiration: Time(time.Now().Add(1 * time.Hour).UTC()), Audience: audience, + customClaims: make(map[string]interface{}), } + + for _, opt := range opts { + opt(j) + } + + return j } func ClaimHash(claim string, sigAlgorithm jose.SignatureAlgorithm) (string, error) { @@ -473,14 +582,14 @@ func AppendClientIDToAudience(clientID string, audience []string) []string { return append(audience, clientID) } -func GenerateJWTProfileToken(assertion *JWTProfileAssertion) (string, error) { - privateKey, err := bytesToPrivateKey(assertion.PrivateKey) +func GenerateJWTProfileToken(assertion JWTProfileAssertionClaims) (string, error) { + privateKey, err := bytesToPrivateKey(assertion.GetPrivateKey()) if err != nil { return "", err } key := jose.SigningKey{ Algorithm: jose.RS256, - Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID}, + Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.GetKeyID()}, } signer, err := jose.NewSigner(key, &jose.SignerOptions{}) if err != nil { diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index 4899c3a..6f9f1af 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -1,6 +1,8 @@ package oidc import ( + "encoding/json" + "fmt" "time" "gopkg.in/square/go-jose.v2" @@ -87,6 +89,50 @@ type JWTTokenRequest struct { Audience Audience `json:"aud"` IssuedAt Time `json:"iat"` ExpiresAt Time `json:"exp"` + + private map[string]interface{} +} + +func (j *JWTTokenRequest) MarshalJSON() ([]byte, error) { + type Alias JWTTokenRequest + a := (*Alias)(j) + + b, err := json.Marshal(a) + if err != nil { + return nil, err + } + + if len(j.private) == 0 { + return b, nil + } + + err = json.Unmarshal(b, &j.private) + if err != nil { + return nil, fmt.Errorf("jws: invalid map of custom claims %v", j.private) + } + + return json.Marshal(j.private) +} + +func (j *JWTTokenRequest) UnmarshalJSON(data []byte) error { + type Alias JWTTokenRequest + a := (*Alias)(j) + + err := json.Unmarshal(data, a) + if err != nil { + return err + } + + err = json.Unmarshal(data, &j.private) + if err != nil { + return err + } + + return nil +} + +func (j *JWTTokenRequest) GetCustomClaim(key string) interface{} { + return j.private[key] } //GetIssuer implements the Claims interface diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index ce63ec8..24b1e7c 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -14,22 +14,39 @@ import ( type JWTProfileVerifier interface { oidc.Verifier Storage() jwtProfileKeyStorage + CheckSubject(request *oidc.JWTTokenRequest) error } type jwtProfileVerifier struct { - storage jwtProfileKeyStorage - issuer string - maxAgeIAT time.Duration - offset time.Duration + storage jwtProfileKeyStorage + subjectCheck func(request *oidc.JWTTokenRequest) error + issuer string + maxAgeIAT time.Duration + offset time.Duration } //NewJWTProfileVerifier creates a oidc.Verifier for JWT Profile assertions (authorization grant and client authentication) -func NewJWTProfileVerifier(storage jwtProfileKeyStorage, issuer string, maxAgeIAT, offset time.Duration) JWTProfileVerifier { - return &jwtProfileVerifier{ - storage: storage, - issuer: issuer, - maxAgeIAT: maxAgeIAT, - offset: offset, +func NewJWTProfileVerifier(storage jwtProfileKeyStorage, issuer string, maxAgeIAT, offset time.Duration, opts ...JWTProfileVerifierOption) JWTProfileVerifier { + j := &jwtProfileVerifier{ + storage: storage, + subjectCheck: SubjectIsIssuer, + issuer: issuer, + maxAgeIAT: maxAgeIAT, + offset: offset, + } + + for _, opt := range opts { + opt(j) + } + + return j +} + +type JWTProfileVerifierOption func(*jwtProfileVerifier) + +func SubjectCheck(check func(request *oidc.JWTTokenRequest) error) JWTProfileVerifierOption { + return func(verifier *jwtProfileVerifier) { + verifier.subjectCheck = check } } @@ -49,6 +66,10 @@ func (v *jwtProfileVerifier) Offset() time.Duration { return v.offset } +func (v *jwtProfileVerifier) CheckSubject(request *oidc.JWTTokenRequest) error { + return v.subjectCheck(request) +} + //VerifyJWTAssertion verifies the assertion string from JWT Profile (authorization grant and client authentication) // //checks audience, exp, iat, signature and that issuer and sub are the same @@ -71,9 +92,8 @@ func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerif return nil, err } - if request.Issuer != request.Subject { - //TODO: implement delegation (openid core / oauth rfc) - return nil, errors.New("delegation not yet implemented, issuer and sub must be identical") + if err = v.CheckSubject(request); err != nil { + return nil, err } keySet := &jwtProfileKeySet{v.Storage(), request.Issuer} @@ -88,6 +108,13 @@ type jwtProfileKeyStorage interface { GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) } +func SubjectIsIssuer(request *oidc.JWTTokenRequest) error { + if request.Issuer != request.Subject { + return errors.New("delegation not allowed, issuer and sub must be identical") + } + return nil +} + type jwtProfileKeySet struct { storage jwtProfileKeyStorage userID string From 58e27e8073cfd02c7a632d062ffd7772171c84b6 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Wed, 30 Jun 2021 14:10:38 +0200 Subject: [PATCH 058/502] simplify KeyProvider interface --- pkg/op/keys.go | 7 +++++-- pkg/op/op.go | 4 ++-- pkg/op/verifier_jwt_profile.go | 7 ++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/op/keys.go b/pkg/op/keys.go index 4b8d607..c4b11d4 100644 --- a/pkg/op/keys.go +++ b/pkg/op/keys.go @@ -1,13 +1,16 @@ package op import ( + "context" "net/http" + "gopkg.in/square/go-jose.v2" + "github.com/caos/oidc/pkg/utils" ) type KeyProvider interface { - Storage() Storage + GetKeySet(context.Context) (*jose.JSONWebKeySet, error) } func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) { @@ -17,7 +20,7 @@ func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) { } func Keys(w http.ResponseWriter, r *http.Request, k KeyProvider) { - keySet, err := k.Storage().GetKeySet(r.Context()) + keySet, err := k.GetKeySet(r.Context()) if err != nil { w.WriteHeader(http.StatusInternalServerError) utils.MarshalJSON(w, err) diff --git a/pkg/op/op.go b/pkg/op/op.go index 518ffdf..241f0ce 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -74,7 +74,7 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router router.HandleFunc(o.IntrospectionEndpoint().Relative(), introspectionHandler(o)) router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o)) router.Handle(o.EndSessionEndpoint().Relative(), intercept(endSessionHandler(o))) - router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o)) + router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o.Storage())) return router } @@ -281,7 +281,7 @@ func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSig if !ok { return nil, errors.New("invalid kid") } - return jws.Verify(key) + return jws.Verify(&key) } type Option func(o *openidProvider) error diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index 24b1e7c..e7784b5 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -122,13 +122,10 @@ type jwtProfileKeySet struct { //VerifySignature implements oidc.KeySet by getting the public key from Storage implementation func (k *jwtProfileKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) { - keyID, alg := oidc.GetKeyIDAndAlg(jws) + keyID, _ := oidc.GetKeyIDAndAlg(jws) key, err := k.storage.GetKeyByIDAndUserID(ctx, keyID, k.userID) if err != nil { return nil, fmt.Errorf("error fetching keys: %w", err) } - if key.Algorithm != alg { - - } - return jws.Verify(&key) + return jws.Verify(key) } From 147c6dca6e8cd0623d73bcc935e3639d24dfd8d4 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Tue, 6 Jul 2021 08:58:35 +0200 Subject: [PATCH 059/502] fixes --- pkg/oidc/token.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index 44c0076..f753120 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -427,7 +427,6 @@ type jwtProfileAssertion struct { func (j *jwtProfileAssertion) MarshalJSON() ([]byte, error) { type Alias jwtProfileAssertion a := (*Alias)(j) - a.Subject = "109050709344825901" b, err := json.Marshal(a) if err != nil { @@ -449,7 +448,6 @@ func (j *jwtProfileAssertion) MarshalJSON() ([]byte, error) { func (j *jwtProfileAssertion) UnmarshalJSON(data []byte) error { type Alias jwtProfileAssertion a := (*Alias)(j) - a.Subject = "109050709344825901" err := json.Unmarshal(data, a) if err != nil { @@ -480,6 +478,9 @@ func (j *jwtProfileAssertion) SetCustomClaim(key string, value interface{}) { } func (j *jwtProfileAssertion) GetCustomClaim(key string) interface{} { + if j.customClaims == nil { + return nil + } return j.customClaims[key] } @@ -524,7 +525,13 @@ func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string, op return GenerateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...)) } -func CustomClaim(key string, value interface{}) func(*jwtProfileAssertion) { +func JWTProfileDelegatedSubject(sub string) func(*jwtProfileAssertion) { + return func(j *jwtProfileAssertion) { + j.Subject = sub + } +} + +func JWTProfileCustomClaim(key string, value interface{}) func(*jwtProfileAssertion) { return func(j *jwtProfileAssertion) { j.customClaims[key] = value } From 8a35b89815484e96e16338cae4e080e30fb81361 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 9 Jul 2021 09:20:03 +0200 Subject: [PATCH 060/502] fix: supported ui locales from config (#107) --- pkg/op/config.go | 4 ++++ pkg/op/discovery.go | 11 +---------- pkg/op/mock/configuration.mock.go | 15 +++++++++++++++ pkg/op/op.go | 6 ++++++ 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/pkg/op/config.go b/pkg/op/config.go index 0e5216b..704cc20 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -5,6 +5,8 @@ import ( "net/url" "os" "strings" + + "golang.org/x/text/language" ) const OidcDevMode = "CAOS_OIDC_DEV" @@ -24,6 +26,8 @@ type Configuration interface { GrantTypeRefreshTokenSupported() bool GrantTypeTokenExchangeSupported() bool GrantTypeJWTAuthorizationSupported() bool + + SupportedUILocales() []language.Tag } func ValidateIssuer(issuer string) error { diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index d057042..807aa20 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -3,8 +3,6 @@ package op import ( "net/http" - "golang.org/x/text/language" - "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/utils" ) @@ -37,7 +35,7 @@ func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfigurati IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(c), ClaimsSupported: SupportedClaims(c), CodeChallengeMethodsSupported: CodeChallengeMethods(c), - UILocalesSupported: UILocales(c), + UILocalesSupported: c.SupportedUILocales(), } } @@ -146,10 +144,3 @@ func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod { } return codeMethods } - -func UILocales(c Configuration) []language.Tag { - return []language.Tag{ - language.English, - language.German, - } -} diff --git a/pkg/op/mock/configuration.mock.go b/pkg/op/mock/configuration.mock.go index da21751..01c2c8d 100644 --- a/pkg/op/mock/configuration.mock.go +++ b/pkg/op/mock/configuration.mock.go @@ -9,6 +9,7 @@ import ( op "github.com/caos/oidc/pkg/op" gomock "github.com/golang/mock/gomock" + language "golang.org/x/text/language" ) // MockConfiguration is a mock of Configuration interface. @@ -188,6 +189,20 @@ func (mr *MockConfigurationMockRecorder) KeysEndpoint() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeysEndpoint", reflect.TypeOf((*MockConfiguration)(nil).KeysEndpoint)) } +// SupportedUILocales mocks base method. +func (m *MockConfiguration) SupportedUILocales() []language.Tag { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SupportedUILocales") + ret0, _ := ret[0].([]language.Tag) + return ret0 +} + +// SupportedUILocales indicates an expected call of SupportedUILocales. +func (mr *MockConfigurationMockRecorder) SupportedUILocales() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportedUILocales", reflect.TypeOf((*MockConfiguration)(nil).SupportedUILocales)) +} + // TokenEndpoint mocks base method. func (m *MockConfiguration) TokenEndpoint() op.Endpoint { m.ctrl.T.Helper() diff --git a/pkg/op/op.go b/pkg/op/op.go index 241f0ce..772b5f7 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -9,6 +9,7 @@ import ( "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/gorilla/schema" + "golang.org/x/text/language" "gopkg.in/square/go-jose.v2" "github.com/caos/oidc/pkg/oidc" @@ -85,6 +86,7 @@ type Config struct { CodeMethodS256 bool AuthMethodPrivateKeyJWT bool GrantTypeRefreshToken bool + SupportedUILocales []language.Tag } type endpoints struct { @@ -202,6 +204,10 @@ func (o *openidProvider) GrantTypeJWTAuthorizationSupported() bool { return true } +func (o *openidProvider) SupportedUILocales() []language.Tag { + return o.config.SupportedUILocales +} + func (o *openidProvider) Storage() Storage { return o.storage } From 1132c9d93db3531ffe0bb349250470fc0c58dea8 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Wed, 21 Jul 2021 08:27:38 +0200 Subject: [PATCH 061/502] fix: removeUserinfoScopes return new slice (without manipulating passed one) (#110) --- pkg/op/token.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pkg/op/token.go b/pkg/op/token.go index 1faffa8..a587f8a 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -155,15 +155,16 @@ func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, v } func removeUserinfoScopes(scopes []string) []string { - for i := len(scopes) - 1; i >= 0; i-- { - if scopes[i] == oidc.ScopeProfile || - scopes[i] == oidc.ScopeEmail || - scopes[i] == oidc.ScopeAddress || - scopes[i] == oidc.ScopePhone { - - scopes[i] = scopes[len(scopes)-1] - scopes[len(scopes)-1] = "" - scopes = scopes[:len(scopes)-1] + newScopeList := make([]string, 0, len(scopes)) + for _, scope := range scopes { + switch scope { + case oidc.ScopeProfile, + oidc.ScopeEmail, + oidc.ScopeAddress, + oidc.ScopePhone: + continue + default: + newScopeList = append(newScopeList, scope) } } return scopes From 3a21b044594bd1fabde7757a7f29c778ec1c63c0 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Tue, 3 Aug 2021 16:52:16 +0200 Subject: [PATCH 062/502] chore: code of conduct (#112) --- CODE_OF_CONDUCT.md | 128 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..b107ae4 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +abuse@zitadel.ch. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. From 3a37300e7a2977bbf7bcd78135e34726a456d4c8 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Tue, 3 Aug 2021 17:00:24 +0200 Subject: [PATCH 063/502] docs: certification comment (#113) --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f7b4365..3a824e6 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,13 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/caos/oidc)](https://goreportcard.com/report/github.com/caos/oidc) [![codecov](https://codecov.io/gh/caos/oidc/branch/master/graph/badge.svg)](https://codecov.io/gh/caos/oidc) -> This project is in beta state. It can AND will continue breaking until version 1.0.0 is released +![openid_certified](https://cloud.githubusercontent.com/assets/1454075/7611268/4d19de32-f97b-11e4-895b-31b2455a7ca6.png) ## What Is It -This project is a easy to use client and server implementation for the `OIDC` (Open ID Connect) standard written for `Go`. +This project is a easy to use client (RP) and server (OP) implementation for the `OIDC` (Open ID Connect) standard written for `Go`. + +The RP is certified for the [basic](https://www.certification.openid.net/plan-detail.html?public=true&plan=uoprP0OO8Z4Qo) and [config](https://www.certification.openid.net/plan-detail.html?public=true&plan=AYSdLbzmWbu9X) profile. Whenever possible we tried to reuse / extend existing packages like `OAuth2 for Go`. @@ -51,7 +53,6 @@ As of 2020 there are not a lot of `OIDC` library's in `Go` which can handle serv ### Goals -- [Certify this library as RP](https://openid.net/certification/#RPs) - [Certify this library as OP](https://openid.net/certification/#OPs) ### Other Go OpenID Connect library's From bd2d17b3f388a185e2c10eec08df108777079845 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Aug 2021 09:28:50 +0200 Subject: [PATCH 064/502] chore(deps): bump codecov/codecov-action from 1 to 2.0.2 (#111) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 1 to 2.0.2. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v1...v2.0.2) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e5ca513..28982a9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov ./pkg/... - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v2.0.2 with: file: ./profile.cov name: codecov-go From 84b2ecc60ed305c03f3bee4b325e9a570dc3699b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Aug 2021 09:29:40 +0200 Subject: [PATCH 065/502] chore(deps): bump github.com/google/uuid from 1.2.0 to 1.3.0 (#108) Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.2.0 to 1.3.0. - [Release notes](https://github.com/google/uuid/releases) - [Commits](https://github.com/google/uuid/compare/v1.2.0...v1.3.0) --- updated-dependencies: - dependency-name: github.com/google/uuid dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e582c70..f68a16a 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.2 // indirect github.com/google/go-github/v31 v31.0.0 - github.com/google/uuid v1.2.0 + github.com/google/uuid v1.3.0 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 github.com/gorilla/schema v1.2.0 diff --git a/go.sum b/go.sum index d08c4eb..a277322 100644 --- a/go.sum +++ b/go.sum @@ -105,8 +105,8 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= From 5c9565c0355a556eedb825f5d251759bfb7a8545 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Aug 2021 04:03:42 +0000 Subject: [PATCH 066/502] chore(deps): bump golang.org/x/text from 0.3.6 to 0.3.7 Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.6 to 0.3.7. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.3.6...v0.3.7) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f68a16a..a340df8 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.7.0 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 - golang.org/x/text v0.3.6 + golang.org/x/text v0.3.7 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/square/go-jose.v2 v2.6.0 ) diff --git a/go.sum b/go.sum index a277322..b3d1da2 100644 --- a/go.sum +++ b/go.sum @@ -272,8 +272,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 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= From 581885afb131f8abdfbbfb844cb529a4791eb2e2 Mon Sep 17 00:00:00 2001 From: Beardo Moore Date: Thu, 26 Aug 2021 20:32:51 +0000 Subject: [PATCH 067/502] task: Ease dev host name constraints This changes the requirements for a issuer hostname to allow anything that is `http`. The reason for this is because the user of the library already has to make a conscious decision to set `CAOS_OIDC_DEV` so they should already understand the risks of not using `https`. The primary motivation for this change is to allow IdPs to be created in a containerized integration test environment. Specifically setting up a docker compose file that starts all parts of the system with a test IdP using this library where the DNS name will not be `localhost`. --- pkg/op/config.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pkg/op/config.go b/pkg/op/config.go index 704cc20..39c84c8 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -4,7 +4,6 @@ import ( "errors" "net/url" "os" - "strings" "golang.org/x/text/language" ) @@ -57,9 +56,5 @@ func devLocalAllowed(url *url.URL) bool { if !b { return b } - return url.Scheme == "http" && - url.Host == "localhost" || - url.Host == "127.0.0.1" || - url.Host == "::1" || - strings.HasPrefix(url.Host, "localhost:") + return url.Scheme == "http" } From 9aa0989dc1e14e5d8bac48e6df638a93cc9d9072 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 27 Aug 2021 14:32:41 +0200 Subject: [PATCH 068/502] chore: enable workflow on PR from forks --- .github/workflows/release.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e5ca513..dfeab93 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,12 @@ name: Release -on: push +on: + push: + branches: + - '**' + tags-ignore: + - '**' + workflow_dispatch: + jobs: test: runs-on: ubuntu-18.04 @@ -21,6 +28,7 @@ jobs: release: runs-on: ubuntu-18.04 needs: [test] + if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: From 87061e0123cb072c45472c361ce2098c51605d2e Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 27 Aug 2021 14:57:48 +0200 Subject: [PATCH 069/502] chore: add 1.17 to matrix build --- .github/workflows/release.yml | 2 +- README.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dfeab93..ecf951c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-18.04 strategy: matrix: - go: ['1.14', '1.15', '1.16'] + go: ['1.14', '1.15', '1.16', '1.17'] name: Go ${{ matrix.go }} test steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index f7b4365..9f898f1 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ For your convenience you can find the relevant standards linked below. | 1.14 | :white_check_mark: | | 1.15 | :white_check_mark: | | 1.16 | :white_check_mark: | +| 1.17 | :white_check_mark: | ## Why another library From d009df3567242837368c93193f3b9f902671ce15 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 27 Aug 2021 15:24:24 +0200 Subject: [PATCH 070/502] chore: add issue templates --- .github/ISSUE_TEMPLATE/bug-report.md | 38 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 1 + .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..49ccc49 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,38 @@ +--- +name: 🐛 Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] + +**Smartphone (please complete the following information):** +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..a49eab2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..118d30e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: 🚀 Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From a3e5d6ba96b0962917a13aa6ef32f6cd7d805e78 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 27 Aug 2021 15:26:41 +0200 Subject: [PATCH 071/502] chore: add CONTRIBUTING.md --- .../{bug-report.md => bug_report.md} | 0 CONTRIBUTING.md | 40 +++++++++++++++++++ 2 files changed, 40 insertions(+) rename .github/ISSUE_TEMPLATE/{bug-report.md => bug_report.md} (100%) create mode 100644 CONTRIBUTING.md diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug_report.md similarity index 100% rename from .github/ISSUE_TEMPLATE/bug-report.md rename to .github/ISSUE_TEMPLATE/bug_report.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f0c8ac7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,40 @@ +# How to contribute to the OIDC SDK for Go + +## Did you find a bug? + +Please file an issue [here](https://github.com/caos/oidc/issues/new?assignees=&labels=bug&template=bug_report.md&title=). + +Bugs are evaluated every day as soon as possible. + +## Enhancement + +Do you miss a feature? Please file an issue [here](https://github.com/caos/oidc/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=) + +Enhancements are discussed and evaluated every Wednesday by the ZITADEL core team. + +## Grab an Issues + +We add the label "good first issue" for problems we think are a good starting point to contribute to the OIDC SDK. + +* [Issues for first time contributors](https://github.com/caos/oidc/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) +* [All issues](https://github.com/caos/oidc/issues) + +### Make a PR + +If you like to contribute fork the OIDC repository. After you implemented the new feature create a PullRequest in the OIDC reposiotry. + +Make sure you use semantic release: + +* feat: New Feature +* fix: Bug Fix +* docs: Documentation + +## Want to use the library? + +Checkout the [examples folder](example) for different client and server implementations. + +Or checkout how we use it ourselves in our OpenSource Identity and Access Management [ZITADEL](https://github.com/caos/zitadel). + +## **Did you find a security flaw?** + +* Please read [Security Policy](SECURITY.md). \ No newline at end of file From 1a2cc86f3ce387b617af0ed983eef0afa985274f Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 27 Aug 2021 15:31:54 +0200 Subject: [PATCH 072/502] chore: change default branch name in .releaserc.js --- .releaserc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.releaserc.js b/.releaserc.js index d9c7f99..c93eb9a 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -1,5 +1,5 @@ module.exports = { - branch: 'master', + branch: 'main', plugins: [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", From 3ed3fa5c0accf6df8c10eeb946e8828376c3ffba Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 27 Aug 2021 15:40:31 +0200 Subject: [PATCH 073/502] chore: fix sem rel configuration --- .releaserc.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.releaserc.js b/.releaserc.js index c93eb9a..2847184 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -1,8 +1,8 @@ module.exports = { - branch: 'main', + branches: ["main"], plugins: [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/github" ] - }; \ No newline at end of file + }; From 353bee9ebe207b35b13ee640efaaa86888e288a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Sep 2021 05:18:02 +0000 Subject: [PATCH 074/502] chore(deps): bump codecov/codecov-action from 2.0.2 to 2.0.3 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 2.0.2 to 2.0.3. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v2.0.2...v2.0.3) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1aeb01..a870158 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov ./pkg/... - - uses: codecov/codecov-action@v2.0.2 + - uses: codecov/codecov-action@v2.0.3 with: file: ./profile.cov name: codecov-go From af3a497b6d7b00166977f6758f8be8ce33c064a5 Mon Sep 17 00:00:00 2001 From: Timo Volkmann Date: Thu, 9 Sep 2021 14:31:31 +0200 Subject: [PATCH 075/502] fix: make pkce code_verifier spec compliant #125 follow recommendations for code_verifier: https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 --- pkg/client/rp/relaying_party.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/client/rp/relaying_party.go b/pkg/client/rp/relaying_party.go index c9c7a8c..a72fa21 100644 --- a/pkg/client/rp/relaying_party.go +++ b/pkg/client/rp/relaying_party.go @@ -2,6 +2,7 @@ package rp import ( "context" + "encoding/base64" "errors" "net/http" "strings" @@ -288,7 +289,7 @@ func AuthURLHandler(stateFn func() string, rp RelyingParty) http.HandlerFunc { //GenerateAndStoreCodeChallenge generates a PKCE code challenge and stores its verifier into a secure cookie func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (string, error) { - codeVerifier := uuid.New().String() + codeVerifier := base64.URLEncoding.EncodeToString([]byte(uuid.New().String())) if err := rp.CookieHandler().SetCookie(w, pkceCode, codeVerifier); err != nil { return "", err } From 99812e0b8e74edd70272f846297b144cdde46282 Mon Sep 17 00:00:00 2001 From: Timo Volkmann <34778004+moximoti@users.noreply.github.com> Date: Mon, 13 Sep 2021 13:56:38 +0200 Subject: [PATCH 076/502] pkce: encode code verifier with base64 without padding Co-authored-by: Livio Amstutz --- pkg/client/rp/relaying_party.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/client/rp/relaying_party.go b/pkg/client/rp/relaying_party.go index a72fa21..669a910 100644 --- a/pkg/client/rp/relaying_party.go +++ b/pkg/client/rp/relaying_party.go @@ -289,7 +289,7 @@ func AuthURLHandler(stateFn func() string, rp RelyingParty) http.HandlerFunc { //GenerateAndStoreCodeChallenge generates a PKCE code challenge and stores its verifier into a secure cookie func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (string, error) { - codeVerifier := base64.URLEncoding.EncodeToString([]byte(uuid.New().String())) + codeVerifier := base64.RawURLEncoding.EncodeToString([]byte(uuid.New().String())) if err := rp.CookieHandler().SetCookie(w, pkceCode, codeVerifier); err != nil { return "", err } From 391b603ccef9e3973b8703226819dba080da3966 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Sep 2021 19:01:38 +0000 Subject: [PATCH 077/502] chore(deps): bump codecov/codecov-action from 2.0.3 to 2.1.0 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 2.0.3 to 2.1.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v2.0.3...v2.1.0) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a870158..6e4ab89 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov ./pkg/... - - uses: codecov/codecov-action@v2.0.3 + - uses: codecov/codecov-action@v2.1.0 with: file: ./profile.cov name: codecov-go From a63fbee93d3729a0fd5f5cbd933ecbd7417dee7e Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Tue, 14 Sep 2021 15:13:44 +0200 Subject: [PATCH 078/502] fix: improve JWS and key verification (#128) * fix: improve JWS and key verification * fix: get remote keys if no cached key matches * fix: get remote keys if no cached key matches * fix exactMatch * fix exactMatch * chore: change default branch name in .releaserc.js --- .releaserc.js | 2 +- go.sum | 18 +++ pkg/client/rp/jwks.go | 95 +++++++++--- pkg/oidc/keyset.go | 38 ++++- pkg/oidc/keyset_test.go | 319 ++++++++++++++++++++++++++++++++++++++++ pkg/oidc/verifier.go | 3 +- pkg/op/op.go | 10 +- 7 files changed, 453 insertions(+), 32 deletions(-) create mode 100644 pkg/oidc/keyset_test.go diff --git a/.releaserc.js b/.releaserc.js index 2847184..6500ace 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -5,4 +5,4 @@ module.exports = { "@semantic-release/release-notes-generator", "@semantic-release/github" ] - }; +}; diff --git a/go.sum b/go.sum index b3d1da2..4ff0c39 100644 --- a/go.sum +++ b/go.sum @@ -50,9 +50,13 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= 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= +github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= +github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -95,6 +99,7 @@ github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6C github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -120,6 +125,7 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -129,21 +135,30 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so= +github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 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= @@ -244,6 +259,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -403,6 +419,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/client/rp/jwks.go b/pkg/client/rp/jwks.go index 98ed501..4062ab4 100644 --- a/pkg/client/rp/jwks.go +++ b/pkg/client/rp/jwks.go @@ -3,26 +3,41 @@ package rp import ( "context" "encoding/json" - "errors" "fmt" "net/http" "sync" "github.com/caos/oidc/pkg/utils" - "gopkg.in/square/go-jose.v2" "github.com/caos/oidc/pkg/oidc" ) -func NewRemoteKeySet(client *http.Client, jwksURL string) oidc.KeySet { - return &remoteKeySet{httpClient: client, jwksURL: jwksURL} +func NewRemoteKeySet(client *http.Client, jwksURL string, opts ...func(*remoteKeySet)) oidc.KeySet { + keyset := &remoteKeySet{httpClient: client, jwksURL: jwksURL} + for _, opt := range opts { + opt(keyset) + } + return keyset +} + +//SkipRemoteCheck will suppress checking for new remote keys if signature validation fails with cached keys +//and no kid header is set in the JWT +// +//this might be handy to save some unnecessary round trips in cases where the JWT does not contain a kid header and +//there is only a single remote key +//please notice that remote keys will then only be fetched if cached keys are empty +func SkipRemoteCheck() func(set *remoteKeySet) { + return func(set *remoteKeySet) { + set.skipRemoteCheck = true + } } type remoteKeySet struct { - jwksURL string - httpClient *http.Client - defaultAlg string + jwksURL string + httpClient *http.Client + defaultAlg string + skipRemoteCheck bool // guard all other fields mu sync.Mutex @@ -72,23 +87,67 @@ func (r *remoteKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSig if alg == "" { alg = r.defaultAlg } - keys := r.keysFromCache() - key, ok := oidc.FindKey(keyID, oidc.KeyUseSignature, alg, keys...) - if ok && keyID != "" { - payload, err := jws.Verify(&key) - return payload, err + payload, err := r.verifySignatureCached(jws, keyID, alg) + if payload != nil { + return payload, nil } + if err != nil { + return nil, err + } + return r.verifySignatureRemote(ctx, jws, keyID, alg) +} +//verifySignatureCached checks for a matching key in the cached key list +// +//if there is only one possible, it tries to verify the signature and will return the payload if successful +// +//it only returns an error if signature validation fails and keys exactMatch which is if either: +// - both kid are empty and skipRemoteCheck is set to true +// - or both (JWT and JWK) kid are equal +// +//otherwise it will return no error (so remote keys will be loaded) +func (r *remoteKeySet) verifySignatureCached(jws *jose.JSONWebSignature, keyID, alg string) ([]byte, error) { + keys := r.keysFromCache() + if len(keys) == 0 { + return nil, nil + } + key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, keys...) + if err != nil { + //no key / multiple found, try with remote keys + return nil, nil //nolint:nilerr + } + payload, err := jws.Verify(&key) + if payload != nil { + return payload, nil + } + if !r.exactMatch(key.KeyID, keyID) { + //no exact key match, try getting better match with remote keys + return nil, nil + } + return nil, fmt.Errorf("signature verification failed: %w", err) +} + +func (r *remoteKeySet) exactMatch(jwkID, jwsID string) bool { + if jwkID == "" && jwsID == "" { + return r.skipRemoteCheck + } + return jwkID == jwsID +} + +func (r *remoteKeySet) verifySignatureRemote(ctx context.Context, jws *jose.JSONWebSignature, keyID, alg string) ([]byte, error) { keys, err := r.keysFromRemote(ctx) if err != nil { - return nil, fmt.Errorf("fetching keys %v", err) + return nil, fmt.Errorf("unable to fetch key for signature validation: %w", err) } - key, ok = oidc.FindKey(keyID, oidc.KeyUseSignature, alg, keys...) - if ok { - payload, err := jws.Verify(&key) - return payload, err + key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, keys...) + if err != nil { + return nil, fmt.Errorf("unable to validate signature: %w", err) } - return nil, errors.New("invalid key") + payload, err := jws.Verify(&key) + if err != nil { + return nil, fmt.Errorf("signature verification failed: %w", err) + } + return payload, nil } func (r *remoteKeySet) keysFromCache() (keys []jose.JSONWebKey) { diff --git a/pkg/oidc/keyset.go b/pkg/oidc/keyset.go index adfffcf..3eca654 100644 --- a/pkg/oidc/keyset.go +++ b/pkg/oidc/keyset.go @@ -5,6 +5,7 @@ import ( "crypto/ecdsa" "crypto/ed25519" "crypto/rsa" + "errors" "gopkg.in/square/go-jose.v2" ) @@ -13,6 +14,11 @@ const ( KeyUseSignature = "sig" ) +var ( + ErrKeyMultiple = errors.New("multiple possible keys match") + ErrKeyNone = errors.New("no possible keys matches") +) + //KeySet represents a set of JSON Web Keys // - remotely fetch via discovery and jwks_uri -> `remoteKeySet` // - held by the OP itself in storage -> `openIDKeySet` @@ -39,20 +45,38 @@ func GetKeyIDAndAlg(jws *jose.JSONWebSignature) (string, string) { //will return the key immediately if matches exact (id, usage, type) // //will return false none or multiple match +// +//deprecated: use FindMatchingKey which will return an error (more specific) instead of just a bool +//moved implementation already to FindMatchingKey func FindKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (jose.JSONWebKey, bool) { + key, err := FindMatchingKey(keyID, use, expectedAlg, keys...) + return key, err == nil +} + +//FindMatchingKey searches the given JSON Web Keys for the requested key ID, usage and key type +// +//will return the key immediately if matches exact (id, usage, type) +// +//will return a specific error if none (ErrKeyNone) or multiple (ErrKeyMultiple) match +func FindMatchingKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (key jose.JSONWebKey, err error) { var validKeys []jose.JSONWebKey - for _, key := range keys { - if key.KeyID == keyID && key.Use == use && algToKeyType(key.Key, expectedAlg) { - if keyID != "" { - return key, true + for _, k := range keys { + if k.Use == use && algToKeyType(k.Key, expectedAlg) { + if k.KeyID == keyID && keyID != "" { + return k, nil + } + if k.KeyID == "" || keyID == "" { + validKeys = append(validKeys, k) } - validKeys = append(validKeys, key) } } if len(validKeys) == 1 { - return validKeys[0], true + return validKeys[0], nil } - return jose.JSONWebKey{}, false + if len(validKeys) > 1 { + return key, ErrKeyMultiple + } + return key, ErrKeyNone } func algToKeyType(key interface{}, alg string) bool { diff --git a/pkg/oidc/keyset_test.go b/pkg/oidc/keyset_test.go new file mode 100644 index 0000000..802edec --- /dev/null +++ b/pkg/oidc/keyset_test.go @@ -0,0 +1,319 @@ +package oidc + +import ( + "crypto/rsa" + "errors" + "reflect" + "testing" + + "gopkg.in/square/go-jose.v2" +) + +func TestFindKey(t *testing.T) { + type args struct { + keyID string + use string + expectedAlg string + keys []jose.JSONWebKey + } + type res struct { + key jose.JSONWebKey + err error + } + tests := []struct { + name string + args args + res res + }{ + { + "no keys, ErrKeyNone", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: nil, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyNone, + }, + }, + { + "single key enc, ErrKeyNone", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "enc", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyNone, + }, + }, + { + "single key wrong algorithm, ErrKeyNone", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PrivateKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyNone, + }, + }, + { + "single key no kid, no jwt kid, match", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + Use: "sig", + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, + { + "single key kid, jwt no kid, match", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + KeyID: "id", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + Use: "sig", + KeyID: "id", + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, + { + "single key no kid, jwt with kid, match", + args{ + keyID: "id", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + Use: "sig", + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, + { + "single key wrong kid, ErrKeyNone", + args{ + keyID: "id", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + KeyID: "id2", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyNone, + }, + }, + { + "multiple keys no kid, jwt no kid, ErrKeyMultiple", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyMultiple, + }, + }, + { + "multiple keys with kid, jwt no kid, ErrKeyMultiple", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + KeyID: "id1", + Key: &rsa.PublicKey{}, + }, + { + Use: "sig", + KeyID: "id2", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyMultiple, + }, + }, + { + "multiple keys, single sig key, jwt no kid, match", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + { + Use: "enc", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + Use: "sig", + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, + { + "multiple keys no kid, jwt with kid, ErrKeyMultiple", + args{ + keyID: "id", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyMultiple, + }, + }, + { + "multiple keys with kid, jwt with kid, match", + args{ + keyID: "id1", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + KeyID: "id1", + Key: &rsa.PublicKey{}, + }, + { + Use: "sig", + KeyID: "id2", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + Use: "sig", + KeyID: "id1", + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, + { + "multiple keys, single sig key, jwt with kid, match", + args{ + keyID: "id1", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + { + Use: "enc", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + Use: "sig", + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := FindMatchingKey(tt.args.keyID, tt.args.use, tt.args.expectedAlg, tt.args.keys...) + if (tt.res.err != nil && !errors.Is(err, tt.res.err)) || (tt.res.err == nil && err != nil) { + t.Errorf("FindKey() error, got = %v, want = %v", err, tt.res.err) + } + if !reflect.DeepEqual(got, tt.res.key) { + t.Errorf("FindKey() got = %v, want %v", got, tt.res.key) + } + }) + } +} diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go index f8470b5..4284d17 100644 --- a/pkg/oidc/verifier.go +++ b/pkg/oidc/verifier.go @@ -39,6 +39,7 @@ var ( ErrSignatureMultiple = errors.New("id_token contains multiple signatures") ErrSignatureUnsupportedAlg = errors.New("signature algorithm not supported") ErrSignatureInvalidPayload = errors.New("signature does not match Payload") + ErrSignatureInvalid = errors.New("invalid signature") ErrExpired = errors.New("token has expired") ErrIatMissing = errors.New("issuedAt of token is missing") ErrIatInFuture = errors.New("issuedAt of token is in the future") @@ -143,7 +144,7 @@ func CheckSignature(ctx context.Context, token string, payload []byte, claims Cl signedPayload, err := set.VerifySignature(ctx, jws) if err != nil { - return err + return fmt.Errorf("%w (%v)", ErrSignatureInvalid, err) } if !bytes.Equal(signedPayload, payload) { diff --git a/pkg/op/op.go b/pkg/op/op.go index 772b5f7..3841227 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -2,7 +2,7 @@ package op import ( "context" - "errors" + "fmt" "net/http" "time" @@ -280,12 +280,12 @@ type openIDKeySet struct { func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { keySet, err := o.Storage.GetKeySet(ctx) if err != nil { - return nil, errors.New("error fetching keys") + return nil, fmt.Errorf("error fetching keys: %w", err) } keyID, alg := oidc.GetKeyIDAndAlg(jws) - key, ok := oidc.FindKey(keyID, oidc.KeyUseSignature, alg, keySet.Keys...) - if !ok { - return nil, errors.New("invalid kid") + key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, keySet.Keys...) + if err != nil { + return nil, fmt.Errorf("invalid signature: %w", err) } return jws.Verify(&key) } From ff2c164057ae862f48a7d36439c1446082c27ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=A8=E6=96=87?= Date: Fri, 8 Oct 2021 14:20:45 +0800 Subject: [PATCH 079/502] fix: improve example & fix userinfo marshal (#132) * fix: example client should track state, call cli.CodeFlow need context * fix: oidc userinfo can UnmarshalJSON with address * rp Discover use client.Discover * add instruction for example to README.md --- README.md | 12 ++++++++++++ example/client/github/github.go | 2 +- example/internal/mock/storage.go | 7 ++++--- pkg/client/rp/relaying_party.go | 5 +++-- pkg/oidc/userinfo.go | 3 ++- pkg/oidc/userinfo_test.go | 26 ++++++++++++++++++++++++++ 6 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 pkg/oidc/userinfo_test.go diff --git a/README.md b/README.md index ef428ed..b90556a 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,18 @@ Whenever possible we tried to reuse / extend existing packages like `OAuth2 for Check the `/example` folder where example code for different scenarios is located. +```bash +# start oidc op server +# oidc discovery http://localhost:9998/.well-known/openid-configuration +CAOS_OIDC_DEV=1 go run github.com/caos/oidc/example/server/default +# start oidc web client +CLIENT_ID=web CLIENT_SECRET=web ISSUER=http://localhost:9998/ SCOPES=openid PORT=5556 go run github.com/caos/oidc/example/client/app +``` + +- browser http://localhost:5556/login will redirect to op server +- input id to login +- redirect to client app display user info + ## Features | | Code Flow | Implicit Flow | Hybrid Flow | Discovery | PKCE | Token Exchange | mTLS | JWT Profile | Refresh Token | diff --git a/example/client/github/github.go b/example/client/github/github.go index f39c40b..35c7723 100644 --- a/example/client/github/github.go +++ b/example/client/github/github.go @@ -43,7 +43,7 @@ func main() { state := func() string { return uuid.New().String() } - token := cli.CodeFlow(relyingParty, callbackPath, port, state) + token := cli.CodeFlow(ctx, relyingParty, callbackPath, port, state) client := github.NewClient(relyingParty.OAuthConfig().Client(ctx, token.Token)) diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index 214ba54..775f757 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -36,6 +36,7 @@ type AuthRequest struct { Nonce string ClientID string CodeChallenge *oidc.CodeChallenge + State string } func (a *AuthRequest) GetACR() string { @@ -98,7 +99,7 @@ func (a *AuthRequest) GetScopes() []string { func (a *AuthRequest) SetCurrentScopes(scopes []string) {} func (a *AuthRequest) GetState() string { - return "" + return a.State } func (a *AuthRequest) GetSubject() string { @@ -120,7 +121,7 @@ func (s *AuthStorage) Health(ctx context.Context) error { } func (s *AuthStorage) CreateAuthRequest(_ context.Context, authReq *oidc.AuthRequest, _ string) (op.AuthRequest, error) { - a = &AuthRequest{ID: "id", ClientID: authReq.ClientID, ResponseType: authReq.ResponseType, Nonce: authReq.Nonce, RedirectURI: authReq.RedirectURI} + a = &AuthRequest{ID: "id", ClientID: authReq.ClientID, ResponseType: authReq.ResponseType, Nonce: authReq.Nonce, RedirectURI: authReq.RedirectURI, State: authReq.State} if authReq.CodeChallenge != "" { a.CodeChallenge = &oidc.CodeChallenge{ Challenge: authReq.CodeChallenge, @@ -212,7 +213,7 @@ func (s *AuthStorage) GetClientByClientID(_ context.Context, id string) (op.Clie accessTokenType = op.AccessTokenTypeJWT responseTypes = []oidc.ResponseType{oidc.ResponseTypeIDToken, oidc.ResponseTypeIDTokenOnly} } - return &ConfClient{ID: id, applicationType: appType, authMethod: authMethod, accessTokenType: accessTokenType, responseTypes: responseTypes, devMode: false}, nil + return &ConfClient{ID: id, applicationType: appType, authMethod: authMethod, accessTokenType: accessTokenType, responseTypes: responseTypes, devMode: false, grantTypes: []oidc.GrantType{oidc.GrantTypeCode}}, nil } func (s *AuthStorage) AuthorizeClientIDSecret(_ context.Context, id string, _ string) error { diff --git a/pkg/client/rp/relaying_party.go b/pkg/client/rp/relaying_party.go index 669a910..b9b568d 100644 --- a/pkg/client/rp/relaying_party.go +++ b/pkg/client/rp/relaying_party.go @@ -170,17 +170,18 @@ func NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, sco return nil, err } } - endpoints, err := Discover(rp.issuer, rp.httpClient) + discoveryConfiguration, err := client.Discover(rp.issuer, rp.httpClient) if err != nil { return nil, err } + endpoints := GetEndpoints(discoveryConfiguration) rp.oauthConfig.Endpoint = endpoints.Endpoint rp.endpoints = endpoints return rp, nil } -//DefaultRPOpts is the type for providing dynamic options to the DefaultRP +//Option is the type for providing dynamic options to the DefaultRP type Option func(*relyingParty) error //WithCookieHandler set a `CookieHandler` for securing the various redirects diff --git a/pkg/oidc/userinfo.go b/pkg/oidc/userinfo.go index 6bc0016..2ae2acb 100644 --- a/pkg/oidc/userinfo.go +++ b/pkg/oidc/userinfo.go @@ -360,6 +360,7 @@ func (i *userinfo) MarshalJSON() ([]byte, error) { func (i *userinfo) UnmarshalJSON(data []byte) error { type Alias userinfo a := &struct { + Address *userInfoAddress `json:"address,omitempty"` *Alias UpdatedAt int64 `json:"update_at,omitempty"` }{ @@ -368,7 +369,7 @@ func (i *userinfo) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &a); err != nil { return err } - + i.Address = a.Address i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC()) if err := json.Unmarshal(data, &i.claims); err != nil { diff --git a/pkg/oidc/userinfo_test.go b/pkg/oidc/userinfo_test.go new file mode 100644 index 0000000..c3c8b7b --- /dev/null +++ b/pkg/oidc/userinfo_test.go @@ -0,0 +1,26 @@ +package oidc + +import ( + "encoding/json" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestUserInfoMarshal(t *testing.T) { + userinfo := NewUserInfo() + userinfo.SetSubject("test") + userinfo.SetAddress(NewUserInfoAddress("Test 789\nPostfach 2", "", "", "", "", "")) + userinfo.SetEmail("test", true) + userinfo.SetPhone("0791234567", true) + userinfo.SetName("Test") + userinfo.AppendClaims("private_claim", "test") + + marshal, err := json.Marshal(userinfo) + out := NewUserInfo() + assert.NoError(t, err) + assert.NoError(t, json.Unmarshal(marshal, out)) + assert.Equal(t, userinfo.GetAddress(), out.GetAddress()) + expected, err := json.Marshal(out) + assert.NoError(t, err) + assert.Equal(t, expected, marshal) +} From eb38b7aa60c8f9c533f40e44e01fa867748853aa Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 8 Oct 2021 08:23:53 +0200 Subject: [PATCH 080/502] chore: build on fork PRs (#133) --- .github/workflows/release.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e4ab89..471c09e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,9 +2,12 @@ name: Release on: push: branches: - - '**' + - main tags-ignore: - '**' + pull_request: + branches: + - '**' workflow_dispatch: jobs: From 292188ba308acbfb7ca55fe5fb37e2712f9b99e9 Mon Sep 17 00:00:00 2001 From: jmillerv <50455719+jmillerv@users.noreply.github.com> Date: Sun, 10 Oct 2021 13:30:24 -0600 Subject: [PATCH 081/502] docs: fix readme typos (#134) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b90556a..eb52037 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ As of 2020 there are not a lot of `OIDC` library's in `Go` which can handle serv - [Certify this library as OP](https://openid.net/certification/#OPs) -### Other Go OpenID Connect library's +### Other Go OpenID Connect libraries [https://github.com/coreos/go-oidc](https://github.com/coreos/go-oidc) @@ -76,7 +76,7 @@ The `go-oidc` does only support `RP` and is not feasible to use as `OP` that's w [https://github.com/ory/fosite](https://github.com/ory/fosite) -We did not choose `fosite` because it implements `OAuth 2.0` on its own and does not rely in the golang provided package. Nonetheless this is a great project. +We did not choose `fosite` because it implements `OAuth 2.0` on its own and does not rely on the golang provided package. Nonetheless this is a great project. ## License From 55ec7d9dd28f825322f6d887c8f2c9535721235e Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Tue, 26 Oct 2021 09:15:02 +0200 Subject: [PATCH 082/502] docs: remove implicit and hybrid flow from supported RP features in readme (#136) * docs: remove implicit flow from supported features in readme * docs: remove implicit flow from supported features in readme Co-authored-by: Florian Forster Co-authored-by: Florian Forster --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eb52037..eaed6a8 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ CLIENT_ID=web CLIENT_SECRET=web ISSUER=http://localhost:9998/ SCOPES=openid PORT | | Code Flow | Implicit Flow | Hybrid Flow | Discovery | PKCE | Token Exchange | mTLS | JWT Profile | Refresh Token | |----------------|-----------|---------------|-------------|-----------|------|----------------|---------|-------------|---------------| -| Relaying Party | yes | yes | not yet | yes | yes | partial | not yet | yes | yes | +| Relaying Party | yes | no[^1] | no | yes | yes | partial | not yet | yes | yes | | Origin Party | yes | yes | not yet | yes | yes | not yet | not yet | yes | yes | ### Resources @@ -85,3 +85,6 @@ The full functionality of this library is and stays open source and free to use See the exact licensing terms [here](./LICENSE) Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + + +[^1]: https://github.com/caos/oidc/issues/135#issuecomment-950563892 From c45f03e144a1bd0420be48c0dde531cbef090b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=A8=E6=96=87?= Date: Thu, 28 Oct 2021 13:06:34 +0800 Subject: [PATCH 083/502] fix: allowed ConcatenateJSON with empty input (#138) --- pkg/utils/marshal.go | 8 ++++++++ pkg/utils/marshal_test.go | 32 +++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/pkg/utils/marshal.go b/pkg/utils/marshal.go index 4f53b4e..8c04588 100644 --- a/pkg/utils/marshal.go +++ b/pkg/utils/marshal.go @@ -29,6 +29,14 @@ func ConcatenateJSON(first, second []byte) ([]byte, error) { if !bytes.HasPrefix(second, []byte{'{'}) { return nil, fmt.Errorf("jws: invalid JSON %s", second) } + // check empty + if len(first) == 2 { + return second, nil + } + if len(second) == 2 { + return first, nil + } + first[len(first)-1] = ',' first = append(first, second[1:]...) return first, nil diff --git a/pkg/utils/marshal_test.go b/pkg/utils/marshal_test.go index f9221f6..bfc8275 100644 --- a/pkg/utils/marshal_test.go +++ b/pkg/utils/marshal_test.go @@ -44,6 +44,36 @@ func TestConcatenateJSON(t *testing.T) { []byte(`{"some": "thing","another": "thing"}`), false, }, + { + "first empty", + args{ + []byte(`{}`), + []byte(`{"some": "thing"}`), + }, + + []byte(`{"some": "thing"}`), + false, + }, + { + "second empty", + args{ + []byte(`{"some": "thing"}`), + []byte(`{}`), + }, + + []byte(`{"some": "thing"}`), + false, + }, + { + "both empty", + args{ + []byte(`{}`), + []byte(`{}`), + }, + + []byte(`{}`), + false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -53,7 +83,7 @@ func TestConcatenateJSON(t *testing.T) { return } if !bytes.Equal(got, tt.want) { - t.Errorf("ConcatenateJSON() got = %v, want %v", got, tt.want) + t.Errorf("ConcatenateJSON() got = %v, want %v", string(got), tt.want) } }) } From 763d3334e7a866e76c9f2d8c944fd34a6cf21e6c Mon Sep 17 00:00:00 2001 From: Witold Konior Date: Tue, 2 Nov 2021 09:14:33 +0100 Subject: [PATCH 084/502] feat: Enable parsing email_verified from string. (#139) * Enable parsing email_verified from string. AWS Cognito will return email_verified from /userinfo endpoint as string. This fix will accept proper boolean values as well as string values. Links for reference: https://forums.aws.amazon.com/thread.jspa?messageID=949441󧳁 https://discuss.elastic.co/t/openid-error-after-authenticating-against-aws-cognito/206018/11 * feat: Enable parsing email_verified from string. --- pkg/oidc/introspection.go | 4 +-- pkg/oidc/userinfo.go | 23 ++++++++++++--- pkg/oidc/userinfo_test.go | 59 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 7 deletions(-) diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index 9b2bad7..8dd1987 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -115,7 +115,7 @@ func (u *introspectionResponse) GetEmail() string { } func (u *introspectionResponse) IsEmailVerified() bool { - return u.EmailVerified + return bool(u.EmailVerified) } func (u *introspectionResponse) GetPhoneNumber() string { @@ -200,7 +200,7 @@ func (u *introspectionResponse) SetPreferredUsername(name string) { func (u *introspectionResponse) SetEmail(email string, verified bool) { u.Email = email - u.EmailVerified = verified + u.EmailVerified = boolString(verified) } func (u *introspectionResponse) SetPhone(phone string, verified bool) { diff --git a/pkg/oidc/userinfo.go b/pkg/oidc/userinfo.go index 2ae2acb..2272421 100644 --- a/pkg/oidc/userinfo.go +++ b/pkg/oidc/userinfo.go @@ -154,7 +154,7 @@ func (u *userinfo) GetEmail() string { } func (u *userinfo) IsEmailVerified() bool { - return u.EmailVerified + return bool(u.EmailVerified) } func (u *userinfo) GetPhoneNumber() string { @@ -235,7 +235,7 @@ func (u *userinfo) SetPreferredUsername(name string) { func (u *userinfo) SetEmail(email string, verified bool) { u.Email = email - u.EmailVerified = verified + u.EmailVerified = boolString(verified) } func (u *userinfo) SetPhone(phone string, verified bool) { @@ -296,8 +296,22 @@ type userInfoProfile struct { } type userInfoEmail struct { - Email string `json:"email,omitempty"` - EmailVerified bool `json:"email_verified,omitempty"` + Email string `json:"email,omitempty"` + + // Handle providers that return email_verified as a string + // https://forums.aws.amazon.com/thread.jspa?messageID=949441󧳁 + // https://discuss.elastic.co/t/openid-error-after-authenticating-against-aws-cognito/206018/11 + EmailVerified boolString `json:"email_verified,omitempty"` +} + +type boolString bool + +func (bs *boolString) UnmarshalJSON(data []byte) error { + if string(data) == "true" || string(data) == `"true"` { + *bs = true + } + + return nil } type userInfoPhone struct { @@ -324,6 +338,7 @@ func NewUserInfoAddress(streetAddress, locality, region, postalCode, country, fo Formatted: formatted, } } + func (i *userinfo) MarshalJSON() ([]byte, error) { type Alias userinfo a := &struct { diff --git a/pkg/oidc/userinfo_test.go b/pkg/oidc/userinfo_test.go index c3c8b7b..f42ff3d 100644 --- a/pkg/oidc/userinfo_test.go +++ b/pkg/oidc/userinfo_test.go @@ -2,8 +2,9 @@ package oidc import ( "encoding/json" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestUserInfoMarshal(t *testing.T) { @@ -24,3 +25,59 @@ func TestUserInfoMarshal(t *testing.T) { assert.NoError(t, err) assert.Equal(t, expected, marshal) } + +func TestUserInfoEmailVerifiedUnmarshal(t *testing.T) { + t.Parallel() + + t.Run("unmarsha email_verified from json bool true", func(t *testing.T) { + jsonBool := []byte(`{"email": "my@email.com", "email_verified": true}`) + + var uie userInfoEmail + + err := json.Unmarshal(jsonBool, &uie) + assert.NoError(t, err) + assert.Equal(t, userInfoEmail{ + Email: "my@email.com", + EmailVerified: true, + }, uie) + }) + + t.Run("unmarsha email_verified from json string true", func(t *testing.T) { + jsonBool := []byte(`{"email": "my@email.com", "email_verified": "true"}`) + + var uie userInfoEmail + + err := json.Unmarshal(jsonBool, &uie) + assert.NoError(t, err) + assert.Equal(t, userInfoEmail{ + Email: "my@email.com", + EmailVerified: true, + }, uie) + }) + + t.Run("unmarsha email_verified from json bool false", func(t *testing.T) { + jsonBool := []byte(`{"email": "my@email.com", "email_verified": false}`) + + var uie userInfoEmail + + err := json.Unmarshal(jsonBool, &uie) + assert.NoError(t, err) + assert.Equal(t, userInfoEmail{ + Email: "my@email.com", + EmailVerified: false, + }, uie) + }) + + t.Run("unmarsha email_verified from json string false", func(t *testing.T) { + jsonBool := []byte(`{"email": "my@email.com", "email_verified": "false"}`) + + var uie userInfoEmail + + err := json.Unmarshal(jsonBool, &uie) + assert.NoError(t, err) + assert.Equal(t, userInfoEmail{ + Email: "my@email.com", + EmailVerified: false, + }, uie) + }) +} From eb10752e485ced36bd996bcb290c6e617f5ea449 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Tue, 2 Nov 2021 13:21:35 +0100 Subject: [PATCH 085/502] feat: Token Revocation, Request Object and OP Certification (#130) FEATURES (and FIXES): - support OAuth 2.0 Token Revocation [RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009) - handle request object using `request` parameter [OIDC Core 1.0 Request Object](https://openid.net/specs/openid-connect-core-1_0.html#RequestObject) - handle response mode - added some information to the discovery endpoint: - revocation_endpoint (added with token revocation) - revocation_endpoint_auth_methods_supported (added with token revocation) - revocation_endpoint_auth_signing_alg_values_supported (added with token revocation) - token_endpoint_auth_signing_alg_values_supported (was missing) - introspection_endpoint_auth_signing_alg_values_supported (was missing) - request_object_signing_alg_values_supported (added with request object) - request_parameter_supported (added with request object) - fixed `removeUserinfoScopes ` now returns the scopes without "userinfo" scopes (profile, email, phone, addedd) [source diff](https://github.com/caos/oidc/pull/130/files#diff-fad50c8c0f065d4dbc49d6c6a38f09c992c8f5d651a479ba00e31b500543559eL170-R171) - improved error handling (pkg/oidc/error.go) and fixed some wrong OAuth errors (e.g. `invalid_grant` instead of `invalid_request`) - improved MarshalJSON and added MarshalJSONWithStatus - removed deprecated PEM decryption from `BytesToPrivateKey` [source diff](https://github.com/caos/oidc/pull/130/files#diff-fe246e428e399ccff599627c71764de51387b60b4df84c67de3febd0954e859bL11-L19) - NewAccessTokenVerifier now uses correct (internal) `accessTokenVerifier` [source diff](https://github.com/caos/oidc/pull/130/files#diff-3a01c7500ead8f35448456ef231c7c22f8d291710936cac91de5edeef52ffc72L52-R52) BREAKING CHANGE: - move functions from `utils` package into separate packages - added various methods to the (OP) `Configuration` interface [source diff](https://github.com/caos/oidc/pull/130/files#diff-2538e0dfc772fdc37f057aecd6fcc2943f516c24e8be794cce0e368a26d20a82R19-R32) - added revocationEndpoint to `WithCustomEndpoints ` [source diff](https://github.com/caos/oidc/pull/130/files#diff-19ae13a743eb7cebbb96492798b1bec556673eb6236b1387e38d722900bae1c3L355-R391) - remove unnecessary context parameter from JWTProfileExchange [source diff](https://github.com/caos/oidc/pull/130/files#diff-4ed8f6affa4a9631fa8a034b3d5752fbb6a819107141aae00029014e950f7b4cL14) --- example/client/app/app.go | 8 +- example/client/github/github.go | 8 +- example/client/service/service.go | 2 +- example/internal/mock/storage.go | 12 +- pkg/client/client.go | 15 +- pkg/client/jwt_profile.go | 7 +- pkg/client/profile/jwt_profile.go | 2 +- pkg/{utils => client/rp/cli}/browser.go | 2 +- pkg/client/rp/cli/cli.go | 6 +- pkg/client/rp/delegation.go | 4 +- pkg/client/rp/jwks.go | 4 +- pkg/client/rp/relaying_party.go | 22 +- pkg/client/rs/resource_server.go | 10 +- pkg/{utils => crypto}/crypto.go | 9 +- pkg/{utils => crypto}/hash.go | 9 +- pkg/{utils => crypto}/key.go | 10 +- pkg/{utils => crypto}/sign.go | 2 +- pkg/{utils => http}/cookie.go | 2 +- pkg/{utils => http}/http.go | 4 +- pkg/{utils => http}/marshal.go | 20 +- pkg/{utils => http}/marshal_test.go | 68 ++++- pkg/oidc/authorization.go | 38 +-- pkg/oidc/code_challenge.go | 4 +- pkg/oidc/discovery.go | 181 +++++++++--- pkg/oidc/error.go | 139 +++++++++ pkg/oidc/introspection.go | 180 ++++++------ pkg/oidc/revocation.go | 6 + pkg/oidc/token.go | 36 +-- pkg/oidc/token_request.go | 4 +- pkg/oidc/types.go | 16 + pkg/oidc/userinfo.go | 30 +- pkg/oidc/verifier.go | 16 +- pkg/op/auth_request.go | 204 ++++++++++--- pkg/op/auth_request_test.go | 370 ++++++++++++++++++------ pkg/op/config.go | 8 + pkg/op/config_test.go | 2 + pkg/op/crypto.go | 6 +- pkg/op/discovery.go | 88 ++++-- pkg/op/discovery_test.go | 5 +- pkg/op/error.go | 89 +----- pkg/op/keys.go | 7 +- pkg/op/keys_test.go | 100 +++++++ pkg/op/mock/authorizer.mock.go | 24 +- pkg/op/mock/configuration.mock.go | 112 +++++++ pkg/op/mock/generate.go | 1 + pkg/op/mock/key.mock.go | 51 ++++ pkg/op/mock/storage.mock.go | 14 + pkg/op/op.go | 64 +++- pkg/op/probes.go | 4 +- pkg/op/session.go | 18 +- pkg/op/storage.go | 3 +- pkg/op/token.go | 11 +- pkg/op/token_code.go | 28 +- pkg/op/token_exchange.go | 21 +- pkg/op/token_intospection.go | 10 +- pkg/op/token_jwt_profile.go | 12 +- pkg/op/token_refresh.go | 27 +- pkg/op/token_request.go | 54 ++-- pkg/op/token_revocation.go | 136 +++++++++ pkg/op/userinfo.go | 11 +- pkg/op/verifier_access_token.go | 2 +- pkg/{utils => strings}/strings.go | 2 +- pkg/{utils => strings}/strings_test.go | 2 +- 63 files changed, 1738 insertions(+), 624 deletions(-) rename pkg/{utils => client/rp/cli}/browser.go (96%) rename pkg/{utils => crypto}/crypto.go (91%) rename pkg/{utils => crypto}/hash.go (80%) rename pkg/{utils => crypto}/key.go (61%) rename pkg/{utils => crypto}/sign.go (97%) rename pkg/{utils => http}/cookie.go (99%) rename pkg/{utils => http}/http.go (97%) rename pkg/{utils => http}/marshal.go (70%) rename pkg/{utils => http}/marshal_test.go (59%) create mode 100644 pkg/oidc/error.go create mode 100644 pkg/oidc/revocation.go create mode 100644 pkg/op/keys_test.go create mode 100644 pkg/op/mock/key.mock.go create mode 100644 pkg/op/token_revocation.go rename pkg/{utils => strings}/strings.go (89%) rename pkg/{utils => strings}/strings_test.go (98%) diff --git a/example/client/app/app.go b/example/client/app/app.go index d835959..6db1597 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -12,13 +12,13 @@ import ( "github.com/sirupsen/logrus" "github.com/caos/oidc/pkg/client/rp" + httphelper "github.com/caos/oidc/pkg/http" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" ) var ( - callbackPath string = "/auth/callback" - key []byte = []byte("test1234test1234") + callbackPath = "/auth/callback" + key = []byte("test1234test1234") ) func main() { @@ -30,7 +30,7 @@ func main() { scopes := strings.Split(os.Getenv("SCOPES"), " ") redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath) - cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) + cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure()) options := []rp.Option{ rp.WithCookieHandler(cookieHandler), diff --git a/example/client/github/github.go b/example/client/github/github.go index 35c7723..45f16c1 100644 --- a/example/client/github/github.go +++ b/example/client/github/github.go @@ -12,12 +12,12 @@ import ( "github.com/caos/oidc/pkg/client/rp" "github.com/caos/oidc/pkg/client/rp/cli" - "github.com/caos/oidc/pkg/utils" + "github.com/caos/oidc/pkg/http" ) var ( - callbackPath string = "/orbctl/github/callback" - key []byte = []byte("test1234test1234") + callbackPath = "/orbctl/github/callback" + key = []byte("test1234test1234") ) func main() { @@ -34,7 +34,7 @@ func main() { } ctx := context.Background() - cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) + cookieHandler := http.NewCookieHandler(key, key, http.WithUnsecure()) relyingParty, err := rp.NewRelyingPartyOAuth(rpConfig, rp.WithCookieHandler(cookieHandler)) if err != nil { fmt.Printf("error creating relaying party: %v", err) diff --git a/example/client/service/service.go b/example/client/service/service.go index 34d959d..818b481 100644 --- a/example/client/service/service.go +++ b/example/client/service/service.go @@ -17,7 +17,7 @@ import ( ) var ( - client *http.Client = http.DefaultClient + client = http.DefaultClient ) func main() { diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index 775f757..570e8a5 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -32,6 +32,7 @@ func NewAuthStorage() op.Storage { type AuthRequest struct { ID string ResponseType oidc.ResponseType + ResponseMode oidc.ResponseMode RedirectURI string Nonce string ClientID string @@ -88,6 +89,10 @@ func (a *AuthRequest) GetResponseType() oidc.ResponseType { return a.ResponseType } +func (a *AuthRequest) GetResponseMode() oidc.ResponseMode { + return a.ResponseMode +} + func (a *AuthRequest) GetScopes() []string { return []string{ "openid", @@ -170,6 +175,11 @@ func (s *AuthStorage) TokenRequestByRefreshToken(ctx context.Context, refreshTok func (s *AuthStorage) TerminateSession(_ context.Context, userID, clientID string) error { return nil } + +func (s *AuthStorage) RevokeToken(ctx context.Context, token string, userID string, clientID string) *oidc.Error { + return nil +} + func (s *AuthStorage) GetSigningKey(_ context.Context, keyCh chan<- jose.SigningKey) { keyCh <- jose.SigningKey{Algorithm: jose.RS256, Key: s.key} } @@ -289,7 +299,7 @@ func (c *ConfClient) AuthMethod() oidc.AuthMethod { } func (c *ConfClient) IDTokenLifetime() time.Duration { - return time.Duration(5 * time.Minute) + return 5 * time.Minute } func (c *ConfClient) AccessTokenType() op.AccessTokenType { return c.accessTokenType diff --git a/pkg/client/client.go b/pkg/client/client.go index fa64b70..1828d1d 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -10,12 +10,13 @@ import ( "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" + "github.com/caos/oidc/pkg/crypto" + httphelper "github.com/caos/oidc/pkg/http" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" ) var ( - Encoder = func() utils.Encoder { + Encoder = func() httphelper.Encoder { e := schema.NewEncoder() e.RegisterEncoder(oidc.SpaceDelimitedArray{}, func(value reflect.Value) string { return value.Interface().(oidc.SpaceDelimitedArray).Encode() @@ -32,7 +33,7 @@ func Discover(issuer string, httpClient *http.Client) (*oidc.DiscoveryConfigurat return nil, err } discoveryConfig := new(oidc.DiscoveryConfiguration) - err = utils.HttpRequest(httpClient, req, &discoveryConfig) + err = httphelper.HttpRequest(httpClient, req, &discoveryConfig) if err != nil { return nil, err } @@ -52,12 +53,12 @@ func CallTokenEndpoint(request interface{}, caller tokenEndpointCaller) (newToke } func callTokenEndpoint(request interface{}, authFn interface{}, caller tokenEndpointCaller) (newToken *oauth2.Token, err error) { - req, err := utils.FormRequest(caller.TokenEndpoint(), request, Encoder, authFn) + req, err := httphelper.FormRequest(caller.TokenEndpoint(), request, Encoder, authFn) if err != nil { return nil, err } tokenRes := new(oidc.AccessTokenResponse) - if err := utils.HttpRequest(caller.HttpClient(), req, &tokenRes); err != nil { + if err := httphelper.HttpRequest(caller.HttpClient(), req, &tokenRes); err != nil { return nil, err } return &oauth2.Token{ @@ -69,7 +70,7 @@ func callTokenEndpoint(request interface{}, authFn interface{}, caller tokenEndp } func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) { - privateKey, err := utils.BytesToPrivateKey(key) + privateKey, err := crypto.BytesToPrivateKey(key) if err != nil { return nil, err } @@ -83,7 +84,7 @@ func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) func SignedJWTProfileAssertion(clientID string, audience []string, expiration time.Duration, signer jose.Signer) (string, error) { iat := time.Now() exp := iat.Add(expiration) - return utils.Sign(&oidc.JWTTokenRequest{ + return crypto.Sign(&oidc.JWTTokenRequest{ Issuer: clientID, Subject: clientID, Audience: audience, diff --git a/pkg/client/jwt_profile.go b/pkg/client/jwt_profile.go index 8095588..e120541 100644 --- a/pkg/client/jwt_profile.go +++ b/pkg/client/jwt_profile.go @@ -1,17 +1,16 @@ package client import ( - "context" "net/url" "golang.org/x/oauth2" + "github.com/caos/oidc/pkg/http" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" ) //JWTProfileExchange handles the oauth2 jwt profile exchange -func JWTProfileExchange(ctx context.Context, jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, caller tokenEndpointCaller) (*oauth2.Token, error) { +func JWTProfileExchange(jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, caller tokenEndpointCaller) (*oauth2.Token, error) { return CallTokenEndpoint(jwtProfileGrantRequest, caller) } @@ -22,7 +21,7 @@ func ClientAssertionCodeOptions(assertion string) []oauth2.AuthCodeOption { } } -func ClientAssertionFormAuthorization(assertion string) utils.FormAuthorization { +func ClientAssertionFormAuthorization(assertion string) http.FormAuthorization { return func(values url.Values) { values.Set("client_assertion", assertion) values.Set("client_assertion_type", oidc.ClientAssertionTypeJWTAssertion) diff --git a/pkg/client/profile/jwt_profile.go b/pkg/client/profile/jwt_profile.go index 46a0fe9..6b7db2c 100644 --- a/pkg/client/profile/jwt_profile.go +++ b/pkg/client/profile/jwt_profile.go @@ -89,5 +89,5 @@ func (j *jwtProfileTokenSource) Token() (*oauth2.Token, error) { if err != nil { return nil, err } - return client.JWTProfileExchange(nil, oidc.NewJWTProfileGrantRequest(assertion, j.scopes...), j) + return client.JWTProfileExchange(oidc.NewJWTProfileGrantRequest(assertion, j.scopes...), j) } diff --git a/pkg/utils/browser.go b/pkg/client/rp/cli/browser.go similarity index 96% rename from pkg/utils/browser.go rename to pkg/client/rp/cli/browser.go index dca75e4..1948427 100644 --- a/pkg/utils/browser.go +++ b/pkg/client/rp/cli/browser.go @@ -1,4 +1,4 @@ -package utils +package cli import ( "fmt" diff --git a/pkg/client/rp/cli/cli.go b/pkg/client/rp/cli/cli.go index 89566eb..aba1546 100644 --- a/pkg/client/rp/cli/cli.go +++ b/pkg/client/rp/cli/cli.go @@ -5,8 +5,8 @@ import ( "net/http" "github.com/caos/oidc/pkg/client/rp" + httphelper "github.com/caos/oidc/pkg/http" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" ) const ( @@ -28,9 +28,9 @@ func CodeFlow(ctx context.Context, relyingParty rp.RelyingParty, callbackPath, p http.Handle(loginPath, rp.AuthURLHandler(stateProvider, relyingParty)) http.Handle(callbackPath, rp.CodeExchangeHandler(callback, relyingParty)) - utils.StartServer(codeflowCtx, ":"+port) + httphelper.StartServer(codeflowCtx, ":"+port) - utils.OpenBrowser("http://localhost:" + port + loginPath) + OpenBrowser("http://localhost:" + port + loginPath) return <-tokenChan } diff --git a/pkg/client/rp/delegation.go b/pkg/client/rp/delegation.go index 3ae6bb6..73edd96 100644 --- a/pkg/client/rp/delegation.go +++ b/pkg/client/rp/delegation.go @@ -5,8 +5,8 @@ import ( ) //DelegationTokenRequest is an implementation of TokenExchangeRequest -//it exchanges a "urn:ietf:params:oauth:token-type:access_token" with an optional -//"urn:ietf:params:oauth:token-type:access_token" actor token for a +//it exchanges an "urn:ietf:params:oauth:token-type:access_token" with an optional +//"urn:ietf:params:oauth:token-type:access_token" actor token for an //"urn:ietf:params:oauth:token-type:access_token" delegation token func DelegationTokenRequest(subjectToken string, opts ...tokenexchange.TokenExchangeOption) *tokenexchange.TokenExchangeRequest { return tokenexchange.NewTokenExchangeRequest(subjectToken, tokenexchange.AccessTokenType, opts...) diff --git a/pkg/client/rp/jwks.go b/pkg/client/rp/jwks.go index 4062ab4..78f9580 100644 --- a/pkg/client/rp/jwks.go +++ b/pkg/client/rp/jwks.go @@ -7,9 +7,9 @@ import ( "net/http" "sync" - "github.com/caos/oidc/pkg/utils" "gopkg.in/square/go-jose.v2" + httphelper "github.com/caos/oidc/pkg/http" "github.com/caos/oidc/pkg/oidc" ) @@ -207,7 +207,7 @@ func (r *remoteKeySet) fetchRemoteKeys(ctx context.Context) ([]jose.JSONWebKey, } keySet := new(jsonWebKeySet) - if err = utils.HttpRequest(r.httpClient, req, keySet); err != nil { + if err = httphelper.HttpRequest(r.httpClient, req, keySet); err != nil { return nil, fmt.Errorf("oidc: failed to get keys: %v", err) } return keySet.Keys, nil diff --git a/pkg/client/rp/relaying_party.go b/pkg/client/rp/relaying_party.go index b9b568d..23c37fc 100644 --- a/pkg/client/rp/relaying_party.go +++ b/pkg/client/rp/relaying_party.go @@ -13,8 +13,8 @@ import ( "gopkg.in/square/go-jose.v2" "github.com/caos/oidc/pkg/client" + httphelper "github.com/caos/oidc/pkg/http" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" ) const ( @@ -39,7 +39,7 @@ type RelyingParty interface { IsPKCE() bool //CookieHandler returns a http cookie handler used for various state transfer cookies - CookieHandler() *utils.CookieHandler + CookieHandler() *httphelper.CookieHandler //HttpClient returns a http client used for calls to the openid provider, e.g. calling token endpoint HttpClient() *http.Client @@ -76,7 +76,7 @@ type relyingParty struct { pkce bool httpClient *http.Client - cookieHandler *utils.CookieHandler + cookieHandler *httphelper.CookieHandler errorHandler func(http.ResponseWriter, *http.Request, string, string, string) idTokenVerifier IDTokenVerifier @@ -96,7 +96,7 @@ func (rp *relyingParty) IsPKCE() bool { return rp.pkce } -func (rp *relyingParty) CookieHandler() *utils.CookieHandler { +func (rp *relyingParty) CookieHandler() *httphelper.CookieHandler { return rp.cookieHandler } @@ -136,7 +136,7 @@ func (rp *relyingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingParty, error) { rp := &relyingParty{ oauthConfig: config, - httpClient: utils.DefaultHTTPClient, + httpClient: httphelper.DefaultHTTPClient, oauth2Only: true, } @@ -161,7 +161,7 @@ func NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, sco RedirectURL: redirectURI, Scopes: scopes, }, - httpClient: utils.DefaultHTTPClient, + httpClient: httphelper.DefaultHTTPClient, oauth2Only: false, } @@ -181,11 +181,11 @@ func NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, sco return rp, nil } -//Option is the type for providing dynamic options to the DefaultRP +//Option is the type for providing dynamic options to the relyingParty type Option func(*relyingParty) error //WithCookieHandler set a `CookieHandler` for securing the various redirects -func WithCookieHandler(cookieHandler *utils.CookieHandler) Option { +func WithCookieHandler(cookieHandler *httphelper.CookieHandler) Option { return func(rp *relyingParty) error { rp.cookieHandler = cookieHandler return nil @@ -195,7 +195,7 @@ func WithCookieHandler(cookieHandler *utils.CookieHandler) Option { //WithPKCE sets the RP to use PKCE (oauth2 code challenge) //it also sets a `CookieHandler` for securing the various redirects //and exchanging the code challenge -func WithPKCE(cookieHandler *utils.CookieHandler) Option { +func WithPKCE(cookieHandler *httphelper.CookieHandler) Option { return func(rp *relyingParty) error { rp.pkce = true rp.cookieHandler = cookieHandler @@ -246,7 +246,7 @@ func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { return Endpoints{}, err } discoveryConfig := new(oidc.DiscoveryConfiguration) - err = utils.HttpRequest(httpClient, req, &discoveryConfig) + err = httphelper.HttpRequest(httpClient, req, &discoveryConfig) if err != nil { return Endpoints{}, err } @@ -395,7 +395,7 @@ func Userinfo(token, tokenType, subject string, rp RelyingParty) (oidc.UserInfo, } req.Header.Set("authorization", tokenType+" "+token) userinfo := oidc.NewUserInfo() - if err := utils.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil { + if err := httphelper.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil { return nil, err } if userinfo.GetSubject() != subject { diff --git a/pkg/client/rs/resource_server.go b/pkg/client/rs/resource_server.go index 551fe88..224442f 100644 --- a/pkg/client/rs/resource_server.go +++ b/pkg/client/rs/resource_server.go @@ -7,8 +7,8 @@ import ( "time" "github.com/caos/oidc/pkg/client" + httphelper "github.com/caos/oidc/pkg/http" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" ) type ResourceServer interface { @@ -39,7 +39,7 @@ func (r *resourceServer) AuthFn() (interface{}, error) { func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option ...Option) (ResourceServer, error) { authorizer := func() (interface{}, error) { - return utils.AuthorizeBasic(clientID, clientSecret), nil + return httphelper.AuthorizeBasic(clientID, clientSecret), nil } return newResourceServer(issuer, authorizer, option...) } @@ -61,7 +61,7 @@ func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, opt func newResourceServer(issuer string, authorizer func() (interface{}, error), options ...Option) (*resourceServer, error) { rs := &resourceServer{ issuer: issuer, - httpClient: utils.DefaultHTTPClient, + httpClient: httphelper.DefaultHTTPClient, } for _, optFunc := range options { optFunc(rs) @@ -111,12 +111,12 @@ func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.Intr if err != nil { return nil, err } - req, err := utils.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, client.Encoder, authFn) + req, err := httphelper.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, client.Encoder, authFn) if err != nil { return nil, err } resp := oidc.NewIntrospectionResponse() - if err := utils.HttpRequest(rp.HttpClient(), req, resp); err != nil { + if err := httphelper.HttpRequest(rp.HttpClient(), req, resp); err != nil { return nil, err } return resp, nil diff --git a/pkg/utils/crypto.go b/pkg/crypto/crypto.go similarity index 91% rename from pkg/utils/crypto.go rename to pkg/crypto/crypto.go index 3ca4963..488d8a4 100644 --- a/pkg/utils/crypto.go +++ b/pkg/crypto/crypto.go @@ -1,4 +1,4 @@ -package utils +package crypto import ( "crypto/aes" @@ -9,6 +9,10 @@ import ( "io" ) +var ( + ErrCipherTextBlockSize = errors.New("ciphertext block size is too short") +) + func EncryptAES(data string, key string) (string, error) { encrypted, err := EncryptBytesAES([]byte(data), key) if err != nil { @@ -55,8 +59,7 @@ func DecryptBytesAES(cipherText []byte, key string) ([]byte, error) { } if len(cipherText) < aes.BlockSize { - err = errors.New("Ciphertext block size is too short!") - return nil, err + return nil, ErrCipherTextBlockSize } iv := cipherText[:aes.BlockSize] cipherText = cipherText[aes.BlockSize:] diff --git a/pkg/utils/hash.go b/pkg/crypto/hash.go similarity index 80% rename from pkg/utils/hash.go rename to pkg/crypto/hash.go index 5dae03c..6529249 100644 --- a/pkg/utils/hash.go +++ b/pkg/crypto/hash.go @@ -1,15 +1,20 @@ -package utils +package crypto import ( "crypto/sha256" "crypto/sha512" "encoding/base64" + "errors" "fmt" "hash" "gopkg.in/square/go-jose.v2" ) +var ( + ErrUnsupportedAlgorithm = errors.New("unsupported signing algorithm") +) + func GetHashAlgorithm(sigAlgorithm jose.SignatureAlgorithm) (hash.Hash, error) { switch sigAlgorithm { case jose.RS256, jose.ES256, jose.PS256: @@ -19,7 +24,7 @@ func GetHashAlgorithm(sigAlgorithm jose.SignatureAlgorithm) (hash.Hash, error) { case jose.RS512, jose.ES512, jose.PS512: return sha512.New(), nil default: - return nil, fmt.Errorf("oidc: unsupported signing algorithm %q", sigAlgorithm) + return nil, fmt.Errorf("%w: %q", ErrUnsupportedAlgorithm, sigAlgorithm) } } diff --git a/pkg/utils/key.go b/pkg/crypto/key.go similarity index 61% rename from pkg/utils/key.go rename to pkg/crypto/key.go index 7965c85..d75d1ab 100644 --- a/pkg/utils/key.go +++ b/pkg/crypto/key.go @@ -1,4 +1,4 @@ -package utils +package crypto import ( "crypto/rsa" @@ -8,15 +8,7 @@ import ( func BytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) { block, _ := pem.Decode(priv) - enc := x509.IsEncryptedPEMBlock(block) b := block.Bytes - var err error - if enc { - b, err = x509.DecryptPEMBlock(block, nil) - if err != nil { - return nil, err - } - } key, err := x509.ParsePKCS1PrivateKey(b) if err != nil { return nil, err diff --git a/pkg/utils/sign.go b/pkg/crypto/sign.go similarity index 97% rename from pkg/utils/sign.go rename to pkg/crypto/sign.go index 5ebac43..a0b9cae 100644 --- a/pkg/utils/sign.go +++ b/pkg/crypto/sign.go @@ -1,4 +1,4 @@ -package utils +package crypto import ( "encoding/json" diff --git a/pkg/utils/cookie.go b/pkg/http/cookie.go similarity index 99% rename from pkg/utils/cookie.go rename to pkg/http/cookie.go index 9e73e08..62ea295 100644 --- a/pkg/utils/cookie.go +++ b/pkg/http/cookie.go @@ -1,4 +1,4 @@ -package utils +package http import ( "errors" diff --git a/pkg/utils/http.go b/pkg/http/http.go similarity index 97% rename from pkg/utils/http.go rename to pkg/http/http.go index 27f96f9..2512707 100644 --- a/pkg/utils/http.go +++ b/pkg/http/http.go @@ -1,4 +1,4 @@ -package utils +package http import ( "context" @@ -14,7 +14,7 @@ import ( var ( DefaultHTTPClient = &http.Client{ - Timeout: time.Duration(30 * time.Second), + Timeout: 30 * time.Second, } ) diff --git a/pkg/utils/marshal.go b/pkg/http/marshal.go similarity index 70% rename from pkg/utils/marshal.go rename to pkg/http/marshal.go index 8c04588..794a28a 100644 --- a/pkg/utils/marshal.go +++ b/pkg/http/marshal.go @@ -1,24 +1,26 @@ -package utils +package http import ( "bytes" "encoding/json" "fmt" "net/http" - - "github.com/sirupsen/logrus" + "reflect" ) func MarshalJSON(w http.ResponseWriter, i interface{}) { - b, err := json.Marshal(i) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + MarshalJSONWithStatus(w, i, http.StatusOK) +} + +func MarshalJSONWithStatus(w http.ResponseWriter, i interface{}, status int) { + w.Header().Set("content-type", "application/json") + w.WriteHeader(status) + if i == nil || (reflect.ValueOf(i).Kind() == reflect.Ptr && reflect.ValueOf(i).IsNil()) { return } - w.Header().Set("content-type", "application/json") - _, err = w.Write(b) + err := json.NewEncoder(w).Encode(i) if err != nil { - logrus.Error("error writing response") + http.Error(w, err.Error(), http.StatusInternalServerError) } } diff --git a/pkg/utils/marshal_test.go b/pkg/http/marshal_test.go similarity index 59% rename from pkg/utils/marshal_test.go rename to pkg/http/marshal_test.go index bfc8275..3838a44 100644 --- a/pkg/utils/marshal_test.go +++ b/pkg/http/marshal_test.go @@ -1,8 +1,11 @@ -package utils +package http import ( "bytes" + "net/http/httptest" "testing" + + "github.com/stretchr/testify/assert" ) func TestConcatenateJSON(t *testing.T) { @@ -88,3 +91,66 @@ func TestConcatenateJSON(t *testing.T) { }) } } + +func TestMarshalJSONWithStatus(t *testing.T) { + type args struct { + i interface{} + status int + } + type res struct { + statusCode int + body string + } + tests := []struct { + name string + args args + res res + }{ + { + "empty ok", + args{ + nil, + 200, + }, + res{ + 200, + "", + }, + }, + { + "string ok", + args{ + "ok", + 200, + }, + res{ + 200, + `"ok" +`, + }, + }, + { + "struct ok", + args{ + struct { + Test string `json:"test"` + }{"ok"}, + 200, + }, + res{ + 200, + `{"test":"ok"} +`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := httptest.NewRecorder() + MarshalJSONWithStatus(w, tt.args.i, tt.args.status) + assert.Equal(t, tt.res.statusCode, w.Result().StatusCode) + assert.Equal(t, "application/json", w.Header().Get("content-type")) + assert.Equal(t, tt.res.body, w.Body.String()) + }) + } +} diff --git a/pkg/oidc/authorization.go b/pkg/oidc/authorization.go index 79d0c1e..e6cfe58 100644 --- a/pkg/oidc/authorization.go +++ b/pkg/oidc/authorization.go @@ -42,6 +42,9 @@ const ( DisplayTouch Display = "touch" DisplayWAP Display = "wap" + ResponseModeQuery ResponseMode = "query" + ResponseModeFragment ResponseMode = "fragment" + //PromptNone (`none`) disallows the Authorization Server to display any authentication or consent user interface pages. //An error (login_required, interaction_required, ...) will be returned if the user is not already authenticated or consent is needed PromptNone = "none" @@ -59,27 +62,28 @@ const ( //AuthRequest according to: //https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest type AuthRequest struct { - ID string - Scopes SpaceDelimitedArray `schema:"scope"` - ResponseType ResponseType `schema:"response_type"` - ClientID string `schema:"client_id"` - RedirectURI string `schema:"redirect_uri"` //TODO: type + Scopes SpaceDelimitedArray `json:"scope" schema:"scope"` + ResponseType ResponseType `json:"response_type" schema:"response_type"` + ClientID string `json:"client_id" schema:"client_id"` + RedirectURI string `json:"redirect_uri" schema:"redirect_uri"` - State string `schema:"state"` + State string `json:"state" schema:"state"` + Nonce string `json:"nonce" schema:"nonce"` - // ResponseMode TODO: ? + ResponseMode ResponseMode `json:"response_mode" schema:"response_mode"` + Display Display `json:"display" schema:"display"` + Prompt SpaceDelimitedArray `json:"prompt" schema:"prompt"` + MaxAge *uint `json:"max_age" schema:"max_age"` + UILocales Locales `json:"ui_locales" schema:"ui_locales"` + IDTokenHint string `json:"id_token_hint" schema:"id_token_hint"` + LoginHint string `json:"login_hint" schema:"login_hint"` + ACRValues []string `json:"acr_values" schema:"acr_values"` - Nonce string `schema:"nonce"` - Display Display `schema:"display"` - Prompt SpaceDelimitedArray `schema:"prompt"` - MaxAge *uint `schema:"max_age"` - UILocales Locales `schema:"ui_locales"` - IDTokenHint string `schema:"id_token_hint"` - LoginHint string `schema:"login_hint"` - ACRValues []string `schema:"acr_values"` + CodeChallenge string `json:"code_challenge" schema:"code_challenge"` + CodeChallengeMethod CodeChallengeMethod `json:"code_challenge_method" schema:"code_challenge_method"` - CodeChallenge string `schema:"code_challenge"` - CodeChallengeMethod CodeChallengeMethod `schema:"code_challenge_method"` + //RequestParam enables OIDC requests to be passed in a single, self-contained parameter (as JWT, called Request Object) + RequestParam string `schema:"request"` } //GetRedirectURI returns the redirect_uri value for the ErrAuthRequest interface diff --git a/pkg/oidc/code_challenge.go b/pkg/oidc/code_challenge.go index 9c4c8a3..4e82feb 100644 --- a/pkg/oidc/code_challenge.go +++ b/pkg/oidc/code_challenge.go @@ -3,7 +3,7 @@ package oidc import ( "crypto/sha256" - "github.com/caos/oidc/pkg/utils" + "github.com/caos/oidc/pkg/crypto" ) const ( @@ -19,7 +19,7 @@ type CodeChallenge struct { } func NewSHACodeChallenge(code string) string { - return utils.HashString(sha256.New(), code, false) + return crypto.HashString(sha256.New(), code, false) } func VerifyCodeChallenge(c *CodeChallenge, codeVerifier string) bool { diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go index acab578..1a92f8d 100644 --- a/pkg/oidc/discovery.go +++ b/pkg/oidc/discovery.go @@ -9,49 +9,144 @@ const ( ) type DiscoveryConfiguration struct { - Issuer string `json:"issuer,omitempty"` - AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` - TokenEndpoint string `json:"token_endpoint,omitempty"` - IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` - UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` - RevocationEndpoint string `json:"revocation_endpoint,omitempty"` - EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` - CheckSessionIframe string `json:"check_session_iframe,omitempty"` - JwksURI string `json:"jwks_uri,omitempty"` - ScopesSupported []string `json:"scopes_supported,omitempty"` - ResponseTypesSupported []string `json:"response_types_supported,omitempty"` - ResponseModesSupported []string `json:"response_modes_supported,omitempty"` - GrantTypesSupported []GrantType `json:"grant_types_supported,omitempty"` - ACRValuesSupported []string `json:"acr_values_supported,omitempty"` - SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` - IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` - IDTokenEncryptionAlgValuesSupported []string `json:"id_token_encryption_alg_values_supported,omitempty"` - IDTokenEncryptionEncValuesSupported []string `json:"id_token_encryption_enc_values_supported,omitempty"` - UserinfoSigningAlgValuesSupported []string `json:"userinfo_signing_alg_values_supported,omitempty"` - UserinfoEncryptionAlgValuesSupported []string `json:"userinfo_encryption_alg_values_supported,omitempty"` - UserinfoEncryptionEncValuesSupported []string `json:"userinfo_encryption_enc_values_supported,omitempty"` - RequestObjectSigningAlgValuesSupported []string `json:"request_object_signing_alg_values_supported,omitempty"` - RequestObjectEncryptionAlgValuesSupported []string `json:"request_object_encryption_alg_values_supported,omitempty"` - RequestObjectEncryptionEncValuesSupported []string `json:"request_object_encryption_enc_values_supported,omitempty"` - TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"` - TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"` - RevocationEndpointAuthMethodsSupported []AuthMethod `json:"revocation_endpoint_auth_methods_supported,omitempty"` - RevocationEndpointAuthSigningAlgValuesSupported []string `json:"revocation_endpoint_auth_signing_alg_values_supported,omitempty"` - IntrospectionEndpointAuthMethodsSupported []AuthMethod `json:"introspection_endpoint_auth_methods_supported,omitempty"` - IntrospectionEndpointAuthSigningAlgValuesSupported []string `json:"introspection_endpoint_auth_signing_alg_values_supported,omitempty"` - DisplayValuesSupported []Display `json:"display_values_supported,omitempty"` - ClaimTypesSupported []string `json:"claim_types_supported,omitempty"` - ClaimsSupported []string `json:"claims_supported,omitempty"` - ClaimsParameterSupported bool `json:"claims_parameter_supported,omitempty"` - CodeChallengeMethodsSupported []CodeChallengeMethod `json:"code_challenge_methods_supported,omitempty"` - ServiceDocumentation string `json:"service_documentation,omitempty"` - ClaimsLocalesSupported []language.Tag `json:"claims_locales_supported,omitempty"` - UILocalesSupported []language.Tag `json:"ui_locales_supported,omitempty"` - RequestParameterSupported bool `json:"request_parameter_supported,omitempty"` - RequestURIParameterSupported bool `json:"request_uri_parameter_supported"` //no omitempty because: If omitted, the default value is true - RequireRequestURIRegistration bool `json:"require_request_uri_registration,omitempty"` - OPPolicyURI string `json:"op_policy_uri,omitempty"` - OPTermsOfServiceURI string `json:"op_tos_uri,omitempty"` + //Issuer is the identifier of the OP and is used in the tokens as `iss` claim. + Issuer string `json:"issuer,omitempty"` + + //AuthorizationEndpoint is the URL of the OAuth 2.0 Authorization Endpoint where all user interactive login start + AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` + + //TokenEndpoint is the URL of the OAuth 2.0 Token Endpoint where all tokens are issued, except when using Implicit Flow + TokenEndpoint string `json:"token_endpoint,omitempty"` + + //IntrospectionEndpoint is the URL of the OAuth 2.0 Introspection Endpoint. + IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` + + //UserinfoEndpoint is the URL where an access_token can be used to retrieve the Userinfo. + UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` + + //RevocationEndpoint is the URL of the OAuth 2.0 Revocation Endpoint. + RevocationEndpoint string `json:"revocation_endpoint,omitempty"` + + //EndSessionEndpoint is a URL where the RP can perform a redirect to request that the End-User be logged out at the OP. + EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` + + //CheckSessionIframe is a URL where the OP provides an iframe that support cross-origin communications for session state information with the RP Client. + CheckSessionIframe string `json:"check_session_iframe,omitempty"` + + //JwksURI is the URL of the JSON Web Key Set. This site contains the signing keys that RPs can use to validate the signature. + //It may also contain the OP's encryption keys that RPs can use to encrypt request to the OP. + JwksURI string `json:"jwks_uri,omitempty"` + + //RegistrationEndpoint is the URL for the Dynamic Client Registration. + RegistrationEndpoint string `json:"registration_endpoint,omitempty"` + + //ScopesSupported lists an array of supported scopes. This list must not include every supported scope by the OP. + ScopesSupported []string `json:"scopes_supported,omitempty"` + + //ResponseTypesSupported contains a list of the OAuth 2.0 response_type values that the OP supports (code, id_token, token id_token, ...). + ResponseTypesSupported []string `json:"response_types_supported,omitempty"` + + //ResponseModesSupported contains a list of the OAuth 2.0 response_mode values that the OP supports. If omitted, the default value is ["query", "fragment"]. + ResponseModesSupported []string `json:"response_modes_supported,omitempty"` + + //GrantTypesSupported contains a list of the OAuth 2.0 grant_type values that the OP supports. If omitted, the default value is ["authorization_code", "implicit"]. + GrantTypesSupported []GrantType `json:"grant_types_supported,omitempty"` + + //ACRValuesSupported contains a list of Authentication Context Class References that the OP supports. + ACRValuesSupported []string `json:"acr_values_supported,omitempty"` + + //SubjectTypesSupported contains a list of Subject Identifier types that the OP supports (pairwise, public). + SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` + + //IDTokenSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the OP for the ID Token. + IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` + + //IDTokenEncryptionAlgValuesSupported contains a list of JWE encryption algorithms (alg values) supported by the OP for the ID Token. + IDTokenEncryptionAlgValuesSupported []string `json:"id_token_encryption_alg_values_supported,omitempty"` + + //IDTokenEncryptionEncValuesSupported contains a list of JWE encryption algorithms (enc values) supported by the OP for the ID Token. + IDTokenEncryptionEncValuesSupported []string `json:"id_token_encryption_enc_values_supported,omitempty"` + + //UserinfoSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the OP for UserInfo Endpoint. + UserinfoSigningAlgValuesSupported []string `json:"userinfo_signing_alg_values_supported,omitempty"` + + //UserinfoEncryptionAlgValuesSupported contains a list of JWE encryption algorithms (alg values) supported by the OP for the UserInfo Endpoint. + UserinfoEncryptionAlgValuesSupported []string `json:"userinfo_encryption_alg_values_supported,omitempty"` + + //UserinfoEncryptionEncValuesSupported contains a list of JWE encryption algorithms (enc values) supported by the OP for the UserInfo Endpoint. + UserinfoEncryptionEncValuesSupported []string `json:"userinfo_encryption_enc_values_supported,omitempty"` + + //RequestObjectSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the OP for Request Objects. + //These algorithms are used both then the Request Object is passed by value (using the request parameter) and when it is passed by reference (using the request_uri parameter). + RequestObjectSigningAlgValuesSupported []string `json:"request_object_signing_alg_values_supported,omitempty"` + + //RequestObjectEncryptionAlgValuesSupported contains a list of JWE encryption algorithms (alg values) supported by the OP for Request Objects. + //These algorithms are used both when the Request Object is passed by value and by reference. + RequestObjectEncryptionAlgValuesSupported []string `json:"request_object_encryption_alg_values_supported,omitempty"` + + //RequestObjectEncryptionEncValuesSupported contains a list of JWE encryption algorithms (enc values) supported by the OP for Request Objects. + //These algorithms are used both when the Request Object is passed by value and by reference. + RequestObjectEncryptionEncValuesSupported []string `json:"request_object_encryption_enc_values_supported,omitempty"` + + //TokenEndpointAuthMethodsSupported contains a list of Client Authentication methods supported by the Token Endpoint. If omitted, the default is client_secret_basic. + TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"` + + //TokenEndpointAuthSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the Token Endpoint + //for the signature of the JWT used to authenticate the Client by private_key_jwt and client_secret_jwt. + TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"` + + //RevocationEndpointAuthMethodsSupported contains a list of Client Authentication methods supported by the Revocation Endpoint. If omitted, the default is client_secret_basic. + RevocationEndpointAuthMethodsSupported []AuthMethod `json:"revocation_endpoint_auth_methods_supported,omitempty"` + + //RevocationEndpointAuthSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the Revocation Endpoint + //for the signature of the JWT used to authenticate the Client by private_key_jwt and client_secret_jwt. + RevocationEndpointAuthSigningAlgValuesSupported []string `json:"revocation_endpoint_auth_signing_alg_values_supported,omitempty"` + + //IntrospectionEndpointAuthMethodsSupported contains a list of Client Authentication methods supported by the Introspection Endpoint. + IntrospectionEndpointAuthMethodsSupported []AuthMethod `json:"introspection_endpoint_auth_methods_supported,omitempty"` + + //IntrospectionEndpointAuthSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the Revocation Endpoint + //for the signature of the JWT used to authenticate the Client by private_key_jwt and client_secret_jwt. + IntrospectionEndpointAuthSigningAlgValuesSupported []string `json:"introspection_endpoint_auth_signing_alg_values_supported,omitempty"` + + //DisplayValuesSupported contains a list of display parameter values that the OP supports (page, popup, touch, wap). + DisplayValuesSupported []Display `json:"display_values_supported,omitempty"` + + //ClaimTypesSupported contains a list of Claim Types that the OP supports (normal, aggregated, distributed). If omitted, the default is normal Claims. + ClaimTypesSupported []string `json:"claim_types_supported,omitempty"` + + //ClaimsSupported contains a list of Claim Names the OP may be able to supply values for. This list might not be exhaustive. + ClaimsSupported []string `json:"claims_supported,omitempty"` + + //ClaimsParameterSupported specifies whether the OP supports use of the `claims` parameter. If omitted, the default is false. + ClaimsParameterSupported bool `json:"claims_parameter_supported,omitempty"` + + //CodeChallengeMethodsSupported contains a list of Proof Key for Code Exchange (PKCE) code challenge methods supported by the OP. + CodeChallengeMethodsSupported []CodeChallengeMethod `json:"code_challenge_methods_supported,omitempty"` + + //ServiceDocumentation is a URL where developers can get information about the OP and its usage. + ServiceDocumentation string `json:"service_documentation,omitempty"` + + //ClaimsLocalesSupported contains a list of BCP47 language tag values that the OP supports for values of Claims returned. + ClaimsLocalesSupported []language.Tag `json:"claims_locales_supported,omitempty"` + + //UILocalesSupported contains a list of BCP47 language tag values that the OP supports for the user interface. + UILocalesSupported []language.Tag `json:"ui_locales_supported,omitempty"` + + //RequestParameterSupported specifies whether the OP supports use of the `request` parameter. If omitted, the default value is false. + RequestParameterSupported bool `json:"request_parameter_supported,omitempty"` + + //RequestURIParameterSupported specifies whether the OP supports use of the `request_uri` parameter. If omitted, the default value is true. (therefore no omitempty) + RequestURIParameterSupported bool `json:"request_uri_parameter_supported"` + + //RequireRequestURIRegistration specifies whether the OP requires any `request_uri` to be pre-registered using the request_uris registration parameter. If omitted, the default value is false. + RequireRequestURIRegistration bool `json:"require_request_uri_registration,omitempty"` + + //OPPolicyURI is a URL the OP provides to the person registering the Client to read about the OP's requirements on how the RP can use the data provided by the OP. + OPPolicyURI string `json:"op_policy_uri,omitempty"` + + //OPTermsOfServiceURI is a URL the OpenID Provider provides to the person registering the Client to read about OpenID Provider's terms of service. + OPTermsOfServiceURI string `json:"op_tos_uri,omitempty"` } type AuthMethod string diff --git a/pkg/oidc/error.go b/pkg/oidc/error.go new file mode 100644 index 0000000..5797a59 --- /dev/null +++ b/pkg/oidc/error.go @@ -0,0 +1,139 @@ +package oidc + +import ( + "errors" + "fmt" +) + +type errorType string + +const ( + InvalidRequest errorType = "invalid_request" + InvalidScope errorType = "invalid_scope" + InvalidClient errorType = "invalid_client" + InvalidGrant errorType = "invalid_grant" + UnauthorizedClient errorType = "unauthorized_client" + UnsupportedGrantType errorType = "unsupported_grant_type" + ServerError errorType = "server_error" + InteractionRequired errorType = "interaction_required" + LoginRequired errorType = "login_required" + RequestNotSupported errorType = "request_not_supported" +) + +var ( + ErrInvalidRequest = func() *Error { + return &Error{ + ErrorType: InvalidRequest, + } + } + ErrInvalidRequestRedirectURI = func() *Error { + return &Error{ + ErrorType: InvalidRequest, + redirectDisabled: true, + } + } + ErrInvalidScope = func() *Error { + return &Error{ + ErrorType: InvalidScope, + } + } + ErrInvalidClient = func() *Error { + return &Error{ + ErrorType: InvalidClient, + } + } + ErrInvalidGrant = func() *Error { + return &Error{ + ErrorType: InvalidGrant, + } + } + ErrUnauthorizedClient = func() *Error { + return &Error{ + ErrorType: UnauthorizedClient, + } + } + ErrUnsupportedGrantType = func() *Error { + return &Error{ + ErrorType: UnsupportedGrantType, + } + } + ErrServerError = func() *Error { + return &Error{ + ErrorType: ServerError, + } + } + ErrInteractionRequired = func() *Error { + return &Error{ + ErrorType: InteractionRequired, + } + } + ErrLoginRequired = func() *Error { + return &Error{ + ErrorType: LoginRequired, + } + } + ErrRequestNotSupported = func() *Error { + return &Error{ + ErrorType: RequestNotSupported, + } + } +) + +type Error struct { + Parent error `json:"-" schema:"-"` + ErrorType errorType `json:"error" schema:"error"` + Description string `json:"error_description,omitempty" schema:"error_description,omitempty"` + State string `json:"state,omitempty" schema:"state,omitempty"` + redirectDisabled bool `schema:"-"` +} + +func (e *Error) Error() string { + message := "ErrorType=" + string(e.ErrorType) + if e.Description != "" { + message += " Description=" + e.Description + } + if e.Parent != nil { + message += " Parent=" + e.Parent.Error() + } + return message +} + +func (e *Error) Unwrap() error { + return e.Parent +} + +func (e *Error) Is(target error) bool { + t, ok := target.(*Error) + if !ok { + return false + } + return e.ErrorType == t.ErrorType && + (e.Description == t.Description || t.Description == "") && + (e.State == t.State || t.State == "") +} + +func (e *Error) WithParent(err error) *Error { + e.Parent = err + return e +} + +func (e *Error) WithDescription(desc string, args ...interface{}) *Error { + e.Description = fmt.Sprintf(desc, args...) + return e +} + +func (e *Error) IsRedirectDisabled() bool { + return e.redirectDisabled +} + +// DefaultToServerError checks if the error is an Error +// if not the provided error will be wrapped into a ServerError +func DefaultToServerError(err error, description string) *Error { + oauth := new(Error) + if ok := errors.As(err, &oauth); !ok { + oauth.ErrorType = ServerError + oauth.Description = description + oauth.Parent = err + } + return oauth +} diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index 8dd1987..6ac2986 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -42,181 +42,181 @@ type introspectionResponse struct { claims map[string]interface{} } -func (u *introspectionResponse) IsActive() bool { - return u.Active +func (i *introspectionResponse) IsActive() bool { + return i.Active } -func (u *introspectionResponse) SetScopes(scope []string) { - u.Scope = scope +func (i *introspectionResponse) SetScopes(scope []string) { + i.Scope = scope } -func (u *introspectionResponse) SetClientID(id string) { - u.ClientID = id +func (i *introspectionResponse) SetClientID(id string) { + i.ClientID = id } -func (u *introspectionResponse) GetSubject() string { - return u.Subject +func (i *introspectionResponse) GetSubject() string { + return i.Subject } -func (u *introspectionResponse) GetName() string { - return u.Name +func (i *introspectionResponse) GetName() string { + return i.Name } -func (u *introspectionResponse) GetGivenName() string { - return u.GivenName +func (i *introspectionResponse) GetGivenName() string { + return i.GivenName } -func (u *introspectionResponse) GetFamilyName() string { - return u.FamilyName +func (i *introspectionResponse) GetFamilyName() string { + return i.FamilyName } -func (u *introspectionResponse) GetMiddleName() string { - return u.MiddleName +func (i *introspectionResponse) GetMiddleName() string { + return i.MiddleName } -func (u *introspectionResponse) GetNickname() string { - return u.Nickname +func (i *introspectionResponse) GetNickname() string { + return i.Nickname } -func (u *introspectionResponse) GetProfile() string { - return u.Profile +func (i *introspectionResponse) GetProfile() string { + return i.Profile } -func (u *introspectionResponse) GetPicture() string { - return u.Picture +func (i *introspectionResponse) GetPicture() string { + return i.Picture } -func (u *introspectionResponse) GetWebsite() string { - return u.Website +func (i *introspectionResponse) GetWebsite() string { + return i.Website } -func (u *introspectionResponse) GetGender() Gender { - return u.Gender +func (i *introspectionResponse) GetGender() Gender { + return i.Gender } -func (u *introspectionResponse) GetBirthdate() string { - return u.Birthdate +func (i *introspectionResponse) GetBirthdate() string { + return i.Birthdate } -func (u *introspectionResponse) GetZoneinfo() string { - return u.Zoneinfo +func (i *introspectionResponse) GetZoneinfo() string { + return i.Zoneinfo } -func (u *introspectionResponse) GetLocale() language.Tag { - return u.Locale +func (i *introspectionResponse) GetLocale() language.Tag { + return i.Locale } -func (u *introspectionResponse) GetPreferredUsername() string { - return u.PreferredUsername +func (i *introspectionResponse) GetPreferredUsername() string { + return i.PreferredUsername } -func (u *introspectionResponse) GetEmail() string { - return u.Email +func (i *introspectionResponse) GetEmail() string { + return i.Email } -func (u *introspectionResponse) IsEmailVerified() bool { - return bool(u.EmailVerified) +func (i *introspectionResponse) IsEmailVerified() bool { + return bool(i.EmailVerified) } -func (u *introspectionResponse) GetPhoneNumber() string { - return u.PhoneNumber +func (i *introspectionResponse) GetPhoneNumber() string { + return i.PhoneNumber } -func (u *introspectionResponse) IsPhoneNumberVerified() bool { - return u.PhoneNumberVerified +func (i *introspectionResponse) IsPhoneNumberVerified() bool { + return i.PhoneNumberVerified } -func (u *introspectionResponse) GetAddress() UserInfoAddress { - return u.Address +func (i *introspectionResponse) GetAddress() UserInfoAddress { + return i.Address } -func (u *introspectionResponse) GetClaim(key string) interface{} { - return u.claims[key] +func (i *introspectionResponse) GetClaim(key string) interface{} { + return i.claims[key] } -func (u *introspectionResponse) SetActive(active bool) { - u.Active = active +func (i *introspectionResponse) SetActive(active bool) { + i.Active = active } -func (u *introspectionResponse) SetSubject(sub string) { - u.Subject = sub +func (i *introspectionResponse) SetSubject(sub string) { + i.Subject = sub } -func (u *introspectionResponse) SetName(name string) { - u.Name = name +func (i *introspectionResponse) SetName(name string) { + i.Name = name } -func (u *introspectionResponse) SetGivenName(name string) { - u.GivenName = name +func (i *introspectionResponse) SetGivenName(name string) { + i.GivenName = name } -func (u *introspectionResponse) SetFamilyName(name string) { - u.FamilyName = name +func (i *introspectionResponse) SetFamilyName(name string) { + i.FamilyName = name } -func (u *introspectionResponse) SetMiddleName(name string) { - u.MiddleName = name +func (i *introspectionResponse) SetMiddleName(name string) { + i.MiddleName = name } -func (u *introspectionResponse) SetNickname(name string) { - u.Nickname = name +func (i *introspectionResponse) SetNickname(name string) { + i.Nickname = name } -func (u *introspectionResponse) SetUpdatedAt(date time.Time) { - u.UpdatedAt = Time(date) +func (i *introspectionResponse) SetUpdatedAt(date time.Time) { + i.UpdatedAt = Time(date) } -func (u *introspectionResponse) SetProfile(profile string) { - u.Profile = profile +func (i *introspectionResponse) SetProfile(profile string) { + i.Profile = profile } -func (u *introspectionResponse) SetPicture(picture string) { - u.Picture = picture +func (i *introspectionResponse) SetPicture(picture string) { + i.Picture = picture } -func (u *introspectionResponse) SetWebsite(website string) { - u.Website = website +func (i *introspectionResponse) SetWebsite(website string) { + i.Website = website } -func (u *introspectionResponse) SetGender(gender Gender) { - u.Gender = gender +func (i *introspectionResponse) SetGender(gender Gender) { + i.Gender = gender } -func (u *introspectionResponse) SetBirthdate(birthdate string) { - u.Birthdate = birthdate +func (i *introspectionResponse) SetBirthdate(birthdate string) { + i.Birthdate = birthdate } -func (u *introspectionResponse) SetZoneinfo(zoneInfo string) { - u.Zoneinfo = zoneInfo +func (i *introspectionResponse) SetZoneinfo(zoneInfo string) { + i.Zoneinfo = zoneInfo } -func (u *introspectionResponse) SetLocale(locale language.Tag) { - u.Locale = locale +func (i *introspectionResponse) SetLocale(locale language.Tag) { + i.Locale = locale } -func (u *introspectionResponse) SetPreferredUsername(name string) { - u.PreferredUsername = name +func (i *introspectionResponse) SetPreferredUsername(name string) { + i.PreferredUsername = name } -func (u *introspectionResponse) SetEmail(email string, verified bool) { - u.Email = email - u.EmailVerified = boolString(verified) +func (i *introspectionResponse) SetEmail(email string, verified bool) { + i.Email = email + i.EmailVerified = boolString(verified) } -func (u *introspectionResponse) SetPhone(phone string, verified bool) { - u.PhoneNumber = phone - u.PhoneNumberVerified = verified +func (i *introspectionResponse) SetPhone(phone string, verified bool) { + i.PhoneNumber = phone + i.PhoneNumberVerified = verified } -func (u *introspectionResponse) SetAddress(address UserInfoAddress) { - u.Address = address +func (i *introspectionResponse) SetAddress(address UserInfoAddress) { + i.Address = address } -func (u *introspectionResponse) AppendClaims(key string, value interface{}) { - if u.claims == nil { - u.claims = make(map[string]interface{}) +func (i *introspectionResponse) AppendClaims(key string, value interface{}) { + if i.claims == nil { + i.claims = make(map[string]interface{}) } - u.claims[key] = value + i.claims[key] = value } func (i *introspectionResponse) MarshalJSON() ([]byte, error) { diff --git a/pkg/oidc/revocation.go b/pkg/oidc/revocation.go new file mode 100644 index 0000000..0a56c61 --- /dev/null +++ b/pkg/oidc/revocation.go @@ -0,0 +1,6 @@ +package oidc + +type RevocationRequest struct { + Token string `schema:"token"` + TokenTypeHint string `schema:"token_type_hint"` +} diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index f753120..e34543e 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -1,10 +1,7 @@ package oidc import ( - "crypto/rsa" - "crypto/x509" "encoding/json" - "encoding/pem" "fmt" "io/ioutil" "time" @@ -12,7 +9,8 @@ import ( "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" - "github.com/caos/oidc/pkg/utils" + "github.com/caos/oidc/pkg/crypto" + "github.com/caos/oidc/pkg/http" ) const ( @@ -188,7 +186,7 @@ func (a *accessTokenClaims) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return utils.ConcatenateJSON(b, info) + return http.ConcatenateJSON(b, info) } func (a *accessTokenClaims) UnmarshalJSON(data []byte) error { @@ -325,7 +323,7 @@ func (t *idTokenClaims) GetSignatureAlgorithm() jose.SignatureAlgorithm { return t.signatureAlg } -//SetSignatureAlgorithm implements the IDTokenClaims interface +//SetAccessTokenHash implements the IDTokenClaims interface func (t *idTokenClaims) SetAccessTokenHash(hash string) { t.AccessTokenHash = hash } @@ -375,7 +373,7 @@ func (t *idTokenClaims) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return utils.ConcatenateJSON(b, info) + return http.ConcatenateJSON(b, info) } func (t *idTokenClaims) UnmarshalJSON(data []byte) error { @@ -572,12 +570,12 @@ func NewJWTProfileAssertion(userID, keyID string, audience []string, key []byte, } func ClaimHash(claim string, sigAlgorithm jose.SignatureAlgorithm) (string, error) { - hash, err := utils.GetHashAlgorithm(sigAlgorithm) + hash, err := crypto.GetHashAlgorithm(sigAlgorithm) if err != nil { return "", err } - return utils.HashString(hash, claim, true), nil + return crypto.HashString(hash, claim, true), nil } func AppendClientIDToAudience(clientID string, audience []string) []string { @@ -590,7 +588,7 @@ func AppendClientIDToAudience(clientID string, audience []string) []string { } func GenerateJWTProfileToken(assertion JWTProfileAssertionClaims) (string, error) { - privateKey, err := bytesToPrivateKey(assertion.GetPrivateKey()) + privateKey, err := crypto.BytesToPrivateKey(assertion.GetPrivateKey()) if err != nil { return "", err } @@ -613,21 +611,3 @@ func GenerateJWTProfileToken(assertion JWTProfileAssertionClaims) (string, error } return signedAssertion.CompactSerialize() } - -func bytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) { - block, _ := pem.Decode(priv) - enc := x509.IsEncryptedPEMBlock(block) - b := block.Bytes - var err error - if enc { - b, err = x509.DecryptPEMBlock(block, nil) - if err != nil { - return nil, err - } - } - key, err := x509.ParsePKCS1PrivateKey(b) - if err != nil { - return nil, err - } - return key, nil -} diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index 6f9f1af..f260f32 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -12,7 +12,7 @@ const ( //GrantTypeCode defines the grant_type `authorization_code` used for the Token Request in the Authorization Code Flow GrantTypeCode GrantType = "authorization_code" - //GrantTypeCode defines the grant_type `refresh_token` used for the Token Request in the Refresh Token Flow + //GrantTypeRefreshToken defines the grant_type `refresh_token` used for the Token Request in the Refresh Token Flow GrantTypeRefreshToken GrantType = "refresh_token" //GrantTypeBearer defines the grant_type `urn:ietf:params:oauth:grant-type:jwt-bearer` used for the JWT Authorization Grant @@ -183,7 +183,7 @@ func (j *JWTTokenRequest) GetSubject() string { return j.Subject } -//GetSubject implements the TokenRequest interface +//GetScopes implements the TokenRequest interface func (j *JWTTokenRequest) GetScopes() []string { return j.Scopes } diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index e72d67c..b6a75f4 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -6,6 +6,7 @@ import ( "time" "golang.org/x/text/language" + "gopkg.in/square/go-jose.v2" ) type Audience []string @@ -66,6 +67,8 @@ type Prompt SpaceDelimitedArray type ResponseType string +type ResponseMode string + func (s SpaceDelimitedArray) Encode() string { return strings.Join(s, " ") } @@ -106,3 +109,16 @@ func (t *Time) UnmarshalJSON(data []byte) error { func (t *Time) MarshalJSON() ([]byte, error) { return json.Marshal(time.Time(*t).UTC().Unix()) } + +type RequestObject struct { + Issuer string `json:"iss"` + Audience Audience `json:"aud"` + AuthRequest +} + +func (r *RequestObject) GetIssuer() string { + return r.Issuer +} + +func (r *RequestObject) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) { +} diff --git a/pkg/oidc/userinfo.go b/pkg/oidc/userinfo.go index 2272421..afc2ad0 100644 --- a/pkg/oidc/userinfo.go +++ b/pkg/oidc/userinfo.go @@ -339,20 +339,20 @@ func NewUserInfoAddress(streetAddress, locality, region, postalCode, country, fo } } -func (i *userinfo) MarshalJSON() ([]byte, error) { +func (u *userinfo) MarshalJSON() ([]byte, error) { type Alias userinfo a := &struct { *Alias Locale interface{} `json:"locale,omitempty"` UpdatedAt int64 `json:"updated_at,omitempty"` }{ - Alias: (*Alias)(i), + Alias: (*Alias)(u), } - if !i.Locale.IsRoot() { - a.Locale = i.Locale + if !u.Locale.IsRoot() { + a.Locale = u.Locale } - if !time.Time(i.UpdatedAt).IsZero() { - a.UpdatedAt = time.Time(i.UpdatedAt).Unix() + if !time.Time(u.UpdatedAt).IsZero() { + a.UpdatedAt = time.Time(u.UpdatedAt).Unix() } b, err := json.Marshal(a) @@ -360,34 +360,34 @@ func (i *userinfo) MarshalJSON() ([]byte, error) { return nil, err } - if len(i.claims) == 0 { + if len(u.claims) == 0 { return b, nil } - err = json.Unmarshal(b, &i.claims) + err = json.Unmarshal(b, &u.claims) if err != nil { - return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims) + return nil, fmt.Errorf("jws: invalid map of custom claims %v", u.claims) } - return json.Marshal(i.claims) + return json.Marshal(u.claims) } -func (i *userinfo) UnmarshalJSON(data []byte) error { +func (u *userinfo) UnmarshalJSON(data []byte) error { type Alias userinfo a := &struct { Address *userInfoAddress `json:"address,omitempty"` *Alias UpdatedAt int64 `json:"update_at,omitempty"` }{ - Alias: (*Alias)(i), + Alias: (*Alias)(u), } if err := json.Unmarshal(data, &a); err != nil { return err } - i.Address = a.Address - i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC()) + u.Address = a.Address + u.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC()) - if err := json.Unmarshal(data, &i.claims); err != nil { + if err := json.Unmarshal(data, &u.claims); err != nil { return err } diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go index 4284d17..9f5335d 100644 --- a/pkg/oidc/verifier.go +++ b/pkg/oidc/verifier.go @@ -12,7 +12,7 @@ import ( "gopkg.in/square/go-jose.v2" - "github.com/caos/oidc/pkg/utils" + str "github.com/caos/oidc/pkg/strings" ) type Claims interface { @@ -25,6 +25,10 @@ type Claims interface { GetAuthenticationContextClassReference() string GetAuthTime() time.Time GetAuthorizedParty() string + ClaimsSignature +} + +type ClaimsSignature interface { SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) } @@ -61,10 +65,10 @@ type Verifier interface { type ACRVerifier func(string) error //DefaultACRVerifier implements `ACRVerifier` returning an error -//if non of the provided values matches the acr claim +//if none of the provided values matches the acr claim func DefaultACRVerifier(possibleValues []string) ACRVerifier { return func(acr string) error { - if !utils.Contains(possibleValues, acr) { + if !str.Contains(possibleValues, acr) { return fmt.Errorf("expected one of: %v, got: %q", possibleValues, acr) } return nil @@ -103,7 +107,7 @@ func CheckIssuer(claims Claims, issuer string) error { } func CheckAudience(claims Claims, clientID string) error { - if !utils.Contains(claims.GetAudience(), clientID) { + if !str.Contains(claims.GetAudience(), clientID) { return fmt.Errorf("%w: Audience must contain client_id %q", ErrAudience, clientID) } @@ -123,7 +127,7 @@ func CheckAuthorizedParty(claims Claims, clientID string) error { return nil } -func CheckSignature(ctx context.Context, token string, payload []byte, claims Claims, supportedSigAlgs []string, set KeySet) error { +func CheckSignature(ctx context.Context, token string, payload []byte, claims ClaimsSignature, supportedSigAlgs []string, set KeySet) error { jws, err := jose.ParseSigned(token) if err != nil { return ErrParse @@ -138,7 +142,7 @@ func CheckSignature(ctx context.Context, token string, payload []byte, claims Cl if len(supportedSigAlgs) == 0 { supportedSigAlgs = []string{"RS256"} } - if !utils.Contains(supportedSigAlgs, sig.Header.Algorithm) { + if !str.Contains(supportedSigAlgs, sig.Header.Algorithm) { return fmt.Errorf("%w: id token signed with unsupported algorithm, expected %q got %q", ErrSignatureUnsupportedAlg, supportedSigAlgs, sig.Header.Algorithm) } diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index fce681f..909b8b0 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -2,7 +2,6 @@ package op import ( "context" - "fmt" "net" "net/http" "net/url" @@ -11,8 +10,9 @@ import ( "github.com/gorilla/mux" + httphelper "github.com/caos/oidc/pkg/http" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" + str "github.com/caos/oidc/pkg/strings" ) type AuthRequest interface { @@ -26,6 +26,7 @@ type AuthRequest interface { GetNonce() string GetRedirectURI() string GetResponseType() oidc.ResponseType + GetResponseMode() oidc.ResponseMode GetScopes() []string GetState() string GetSubject() string @@ -34,16 +35,17 @@ type AuthRequest interface { type Authorizer interface { Storage() Storage - Decoder() utils.Decoder - Encoder() utils.Encoder + Decoder() httphelper.Decoder + Encoder() httphelper.Encoder Signer() Signer IDTokenHintVerifier() IDTokenHintVerifier Crypto() Crypto Issuer() string + RequestObjectSupported() bool } //AuthorizeValidator is an extension of Authorizer interface -//implementing it's own validation mechanism for the auth request +//implementing its own validation mechanism for the auth request type AuthorizeValidator interface { Authorizer ValidateAuthRequest(context.Context, *oidc.AuthRequest, Storage, IDTokenHintVerifier) (string, error) @@ -69,6 +71,13 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { AuthRequestError(w, r, authReq, err, authorizer.Encoder()) return } + if authReq.RequestParam != "" && authorizer.RequestObjectSupported() { + authReq, err = ParseRequestObject(r.Context(), authReq, authorizer.Storage(), authorizer.Issuer()) + if err != nil { + AuthRequestError(w, r, authReq, err, authorizer.Encoder()) + return + } + } validation := ValidateAuthRequest if validater, ok := authorizer.(AuthorizeValidator); ok { validation = validater.ValidateAuthRequest @@ -78,33 +87,114 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { AuthRequestError(w, r, authReq, err, authorizer.Encoder()) return } + if authReq.RequestParam != "" { + AuthRequestError(w, r, authReq, oidc.ErrRequestNotSupported(), authorizer.Encoder()) + return + } req, err := authorizer.Storage().CreateAuthRequest(r.Context(), authReq, userID) if err != nil { - AuthRequestError(w, r, authReq, err, authorizer.Encoder()) + AuthRequestError(w, r, authReq, oidc.DefaultToServerError(err, "unable to save auth request"), authorizer.Encoder()) return } client, err := authorizer.Storage().GetClientByClientID(r.Context(), req.GetClientID()) if err != nil { - AuthRequestError(w, r, req, err, authorizer.Encoder()) + AuthRequestError(w, r, req, oidc.DefaultToServerError(err, "unable to retrieve client by id"), authorizer.Encoder()) return } RedirectToLogin(req.GetID(), client, w, r) } -//ParseAuthorizeRequest parsed the http request into a oidc.AuthRequest -func ParseAuthorizeRequest(r *http.Request, decoder utils.Decoder) (*oidc.AuthRequest, error) { +//ParseAuthorizeRequest parsed the http request into an oidc.AuthRequest +func ParseAuthorizeRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.AuthRequest, error) { err := r.ParseForm() if err != nil { - return nil, ErrInvalidRequest("cannot parse form") + return nil, oidc.ErrInvalidRequest().WithDescription("cannot parse form").WithParent(err) } authReq := new(oidc.AuthRequest) err = decoder.Decode(authReq, r.Form) if err != nil { - return nil, ErrInvalidRequest(fmt.Sprintf("cannot parse auth request: %v", err)) + return nil, oidc.ErrInvalidRequest().WithDescription("cannot parse auth request").WithParent(err) } return authReq, nil } +//ParseRequestObject parse the `request` parameter, validates the token including the signature +//and copies the token claims into the auth request +func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, issuer string) (*oidc.AuthRequest, error) { + requestObject := new(oidc.RequestObject) + payload, err := oidc.ParseToken(authReq.RequestParam, requestObject) + if err != nil { + return nil, err + } + + if requestObject.ClientID != "" && requestObject.ClientID != authReq.ClientID { + return authReq, oidc.ErrInvalidRequest() + } + if requestObject.ResponseType != "" && requestObject.ResponseType != authReq.ResponseType { + return authReq, oidc.ErrInvalidRequest() + } + if requestObject.Issuer != requestObject.ClientID { + return authReq, oidc.ErrInvalidRequest() + } + if !str.Contains(requestObject.Audience, issuer) { + return authReq, oidc.ErrInvalidRequest() + } + keySet := &jwtProfileKeySet{storage, requestObject.Issuer} + if err = oidc.CheckSignature(ctx, authReq.RequestParam, payload, requestObject, nil, keySet); err != nil { + return authReq, err + } + CopyRequestObjectToAuthRequest(authReq, requestObject) + return authReq, nil +} + +//CopyRequestObjectToAuthRequest overwrites present values from the Request Object into the auth request +//and clears the `RequestParam` of the auth request +func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oidc.RequestObject) { + if str.Contains(authReq.Scopes, oidc.ScopeOpenID) && len(requestObject.Scopes) > 0 { + authReq.Scopes = requestObject.Scopes + } + if requestObject.RedirectURI != "" { + authReq.RedirectURI = requestObject.RedirectURI + } + if requestObject.State != "" { + authReq.State = requestObject.State + } + if requestObject.ResponseMode != "" { + authReq.ResponseMode = requestObject.ResponseMode + } + if requestObject.Nonce != "" { + authReq.Nonce = requestObject.Nonce + } + if requestObject.Display != "" { + authReq.Display = requestObject.Display + } + if len(requestObject.Prompt) > 0 { + authReq.Prompt = requestObject.Prompt + } + if requestObject.MaxAge != nil { + authReq.MaxAge = requestObject.MaxAge + } + if len(requestObject.UILocales) > 0 { + authReq.UILocales = requestObject.UILocales + } + if requestObject.IDTokenHint != "" { + authReq.IDTokenHint = requestObject.IDTokenHint + } + if requestObject.LoginHint != "" { + authReq.LoginHint = requestObject.LoginHint + } + if len(requestObject.ACRValues) > 0 { + authReq.ACRValues = requestObject.ACRValues + } + if requestObject.CodeChallenge != "" { + authReq.CodeChallenge = requestObject.CodeChallenge + } + if requestObject.CodeChallengeMethod != "" { + authReq.CodeChallengeMethod = requestObject.CodeChallengeMethod + } + authReq.RequestParam = "" +} + //ValidateAuthRequest validates the authorize parameters and returns the userID of the id_token_hint if passed func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier IDTokenHintVerifier) (sub string, err error) { authReq.MaxAge, err = ValidateAuthReqPrompt(authReq.Prompt, authReq.MaxAge) @@ -113,7 +203,7 @@ func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage } client, err := storage.GetClientByClientID(ctx, authReq.ClientID) if err != nil { - return "", ErrServerError(err.Error()) + return "", oidc.DefaultToServerError(err, "unable to retrieve client by id") } authReq.Scopes, err = ValidateAuthReqScopes(client, authReq.Scopes) if err != nil { @@ -132,7 +222,7 @@ func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage func ValidateAuthReqPrompt(prompts []string, maxAge *uint) (_ *uint, err error) { for _, prompt := range prompts { if prompt == oidc.PromptNone && len(prompts) > 1 { - return nil, ErrInvalidRequest("The prompt parameter `none` must only be used as a single value") + return nil, oidc.ErrInvalidRequest().WithDescription("The prompt parameter `none` must only be used as a single value") } if prompt == oidc.PromptLogin { maxAge = oidc.NewMaxAge(0) @@ -144,7 +234,9 @@ func ValidateAuthReqPrompt(prompts []string, maxAge *uint) (_ *uint, err error) //ValidateAuthReqScopes validates the passed scopes func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) { if len(scopes) == 0 { - return nil, ErrInvalidRequest("The scope of your request is missing. Please ensure some scopes are requested. If you have any questions, you may contact the administrator of the application.") + return nil, oidc.ErrInvalidRequest(). + WithDescription("The scope of your request is missing. Please ensure some scopes are requested. " + + "If you have any questions, you may contact the administrator of the application.") } openID := false for i := len(scopes) - 1; i >= 0; i-- { @@ -165,7 +257,9 @@ func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) { } } if !openID { - return nil, ErrInvalidRequest("The scope openid is missing in your request. Please ensure the scope openid is added to the request. If you have any questions, you may contact the administrator of the application.") + return nil, oidc.ErrInvalidScope().WithDescription("The scope openid is missing in your request. " + + "Please ensure the scope openid is added to the request. " + + "If you have any questions, you may contact the administrator of the application.") } return scopes, nil @@ -174,19 +268,23 @@ func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) { //ValidateAuthReqRedirectURI validates the passed redirect_uri and response_type to the registered uris and client type func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.ResponseType) error { if uri == "" { - return ErrInvalidRequestRedirectURI("The redirect_uri is missing in the request. Please ensure it is added to the request. If you have any questions, you may contact the administrator of the application.") + return oidc.ErrInvalidRequestRedirectURI().WithDescription("The redirect_uri is missing in the request. " + + "Please ensure it is added to the request. If you have any questions, you may contact the administrator of the application.") } if strings.HasPrefix(uri, "https://") { - if !utils.Contains(client.RedirectURIs(), uri) { - return ErrInvalidRequestRedirectURI("The requested redirect_uri is missing in the client configuration. If you have any questions, you may contact the administrator of the application.") + if !str.Contains(client.RedirectURIs(), uri) { + return oidc.ErrInvalidRequestRedirectURI(). + WithDescription("The requested redirect_uri is missing in the client configuration. " + + "If you have any questions, you may contact the administrator of the application.") } return nil } if client.ApplicationType() == ApplicationTypeNative { return validateAuthReqRedirectURINative(client, uri, responseType) } - if !utils.Contains(client.RedirectURIs(), uri) { - return ErrInvalidRequestRedirectURI("The requested redirect_uri is missing in the client configuration. If you have any questions, you may contact the administrator of the application.") + if !str.Contains(client.RedirectURIs(), uri) { + return oidc.ErrInvalidRequestRedirectURI().WithDescription("The requested redirect_uri is missing in the client configuration. " + + "If you have any questions, you may contact the administrator of the application.") } if strings.HasPrefix(uri, "http://") { if client.DevMode() { @@ -195,23 +293,27 @@ func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.Res if responseType == oidc.ResponseTypeCode && IsConfidentialType(client) { return nil } - return ErrInvalidRequest("This client's redirect_uri is http and is not allowed. If you have any questions, you may contact the administrator of the application.") + return oidc.ErrInvalidRequestRedirectURI().WithDescription("This client's redirect_uri is http and is not allowed. " + + "If you have any questions, you may contact the administrator of the application.") } - return ErrInvalidRequest("This client's redirect_uri is using a custom schema and is not allowed. If you have any questions, you may contact the administrator of the application.") + return oidc.ErrInvalidRequestRedirectURI().WithDescription("This client's redirect_uri is using a custom schema and is not allowed. " + + "If you have any questions, you may contact the administrator of the application.") } //ValidateAuthReqRedirectURINative validates the passed redirect_uri and response_type to the registered uris and client type func validateAuthReqRedirectURINative(client Client, uri string, responseType oidc.ResponseType) error { parsedURL, isLoopback := HTTPLoopbackOrLocalhost(uri) isCustomSchema := !strings.HasPrefix(uri, "http://") - if utils.Contains(client.RedirectURIs(), uri) { + if str.Contains(client.RedirectURIs(), uri) { if isLoopback || isCustomSchema { return nil } - return ErrInvalidRequest("This client's redirect_uri is http and is not allowed. If you have any questions, you may contact the administrator of the application.") + return oidc.ErrInvalidRequestRedirectURI().WithDescription("This client's redirect_uri is http and is not allowed. " + + "If you have any questions, you may contact the administrator of the application.") } if !isLoopback { - return ErrInvalidRequestRedirectURI("The requested redirect_uri is missing in the client configuration. If you have any questions, you may contact the administrator of the application.") + return oidc.ErrInvalidRequestRedirectURI().WithDescription("The requested redirect_uri is missing in the client configuration. " + + "If you have any questions, you may contact the administrator of the application.") } for _, uri := range client.RedirectURIs() { redirectURI, ok := HTTPLoopbackOrLocalhost(uri) @@ -219,7 +321,8 @@ func validateAuthReqRedirectURINative(client Client, uri string, responseType oi return nil } } - return ErrInvalidRequestRedirectURI("The requested redirect_uri is missing in the client configuration. If you have any questions, you may contact the administrator of the application.") + return oidc.ErrInvalidRequestRedirectURI().WithDescription("The requested redirect_uri is missing in the client configuration." + + " If you have any questions, you may contact the administrator of the application.") } func equalURI(url1, url2 *url.URL) bool { @@ -241,10 +344,12 @@ func HTTPLoopbackOrLocalhost(rawurl string) (*url.URL, bool) { //ValidateAuthReqResponseType validates the passed response_type to the registered response types func ValidateAuthReqResponseType(client Client, responseType oidc.ResponseType) error { if responseType == "" { - return ErrInvalidRequest("The response type is missing in your request. If you have any questions, you may contact the administrator of the application.") + return oidc.ErrInvalidRequest().WithDescription("The response type is missing in your request. " + + "If you have any questions, you may contact the administrator of the application.") } if !ContainsResponseType(client.ResponseTypes(), responseType) { - return ErrInvalidRequest("The requested response type is missing in the client configuration. If you have any questions, you may contact the administrator of the application.") + return oidc.ErrUnauthorizedClient().WithDescription("The requested response type is missing in the client configuration. " + + "If you have any questions, you may contact the administrator of the application.") } return nil } @@ -257,7 +362,8 @@ func ValidateAuthReqIDTokenHint(ctx context.Context, idTokenHint string, verifie } claims, err := VerifyIDTokenHint(ctx, idTokenHint, verifier) if err != nil { - return "", ErrInvalidRequest("The id_token_hint is invalid. If you have any questions, you may contact the administrator of the application.") + return "", oidc.ErrLoginRequired().WithDescription("The id_token_hint is invalid. " + + "If you have any questions, you may contact the administrator of the application.") } return claims.GetSubject(), nil } @@ -279,7 +385,9 @@ func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Author return } if !authReq.Done() { - AuthRequestError(w, r, authReq, ErrInteractionRequired("Unfortunately, the user may is not logged in and/or additional interaction is required."), authorizer.Encoder()) + AuthRequestError(w, r, authReq, + oidc.ErrInteractionRequired().WithDescription("Unfortunately, the user may be not logged in and/or additional interaction is required."), + authorizer.Encoder()) return } AuthResponse(authReq, authorizer, w, r) @@ -306,9 +414,17 @@ func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthReques AuthRequestError(w, r, authReq, err, authorizer.Encoder()) return } - callback := fmt.Sprintf("%s?code=%s", authReq.GetRedirectURI(), code) - if authReq.GetState() != "" { - callback = callback + "&state=" + authReq.GetState() + codeResponse := struct { + code string + state string + }{ + code: code, + state: authReq.GetState(), + } + callback, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), &codeResponse, authorizer.Encoder()) + if err != nil { + AuthRequestError(w, r, authReq, err, authorizer.Encoder()) + return } http.Redirect(w, r, callback, http.StatusFound) } @@ -321,12 +437,11 @@ func AuthResponseToken(w http.ResponseWriter, r *http.Request, authReq AuthReque AuthRequestError(w, r, authReq, err, authorizer.Encoder()) return } - params, err := utils.URLEncodeResponse(resp, authorizer.Encoder()) + callback, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), resp, authorizer.Encoder()) if err != nil { AuthRequestError(w, r, authReq, err, authorizer.Encoder()) return } - callback := fmt.Sprintf("%s#%s", authReq.GetRedirectURI(), params) http.Redirect(w, r, callback, http.StatusFound) } @@ -346,3 +461,22 @@ func CreateAuthRequestCode(ctx context.Context, authReq AuthRequest, storage Sto func BuildAuthRequestCode(authReq AuthRequest, crypto Crypto) (string, error) { return crypto.Encrypt(authReq.GetID()) } + +//AuthResponseURL encodes the authorization response (successful and error) and sets it as query or fragment values +//depending on the response_mode and response_type +func AuthResponseURL(redirectURI string, responseType oidc.ResponseType, responseMode oidc.ResponseMode, response interface{}, encoder httphelper.Encoder) (string, error) { + params, err := httphelper.URLEncodeResponse(response, encoder) + if err != nil { + return "", oidc.ErrServerError().WithParent(err) + } + if responseMode == oidc.ResponseModeQuery { + return redirectURI + "?" + params, nil + } + if responseMode == oidc.ResponseModeFragment { + return redirectURI + "#" + params, nil + } + if responseType == "" || responseType == oidc.ResponseTypeCode { + return redirectURI + "?" + params, nil + } + return redirectURI + "#" + params, nil +} diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index 40e1a8a..7259ec7 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -1,6 +1,8 @@ package op_test import ( + "context" + "errors" "net/http" "net/http/httptest" "net/url" @@ -11,10 +13,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + httphelper "github.com/caos/oidc/pkg/http" "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/op" "github.com/caos/oidc/pkg/op/mock" - "github.com/caos/oidc/pkg/utils" ) // @@ -75,7 +77,7 @@ import ( func TestParseAuthorizeRequest(t *testing.T) { type args struct { r *http.Request - decoder utils.Decoder + decoder httphelper.Decoder } type res struct { want *oidc.AuthRequest @@ -101,7 +103,7 @@ func TestParseAuthorizeRequest(t *testing.T) { "decoding error", args{ &http.Request{URL: &url.URL{RawQuery: "unknown=value"}}, - func() utils.Decoder { + func() httphelper.Decoder { decoder := schema.NewDecoder() decoder.IgnoreUnknownKeys(false) return decoder @@ -116,7 +118,7 @@ func TestParseAuthorizeRequest(t *testing.T) { "parsing ok", args{ &http.Request{URL: &url.URL{RawQuery: "scope=openid"}}, - func() utils.Decoder { + func() httphelper.Decoder { decoder := schema.NewDecoder() decoder.IgnoreUnknownKeys(false) return decoder @@ -150,44 +152,138 @@ func TestValidateAuthRequest(t *testing.T) { tests := []struct { name string args args - wantErr bool + wantErr error }{ - //TODO: - // { - // "oauth2 spec" - // } { "scope missing fails", args{&oidc.AuthRequest{}, mock.NewMockStorageExpectValidClientID(t), nil}, - true, + oidc.ErrInvalidRequest(), }, { "scope openid missing fails", args{&oidc.AuthRequest{Scopes: []string{"profile"}}, mock.NewMockStorageExpectValidClientID(t), nil}, - true, + oidc.ErrInvalidScope(), }, { "response_type missing fails", args{&oidc.AuthRequest{Scopes: []string{"openid"}}, mock.NewMockStorageExpectValidClientID(t), nil}, - true, + oidc.ErrInvalidRequest(), }, { "client_id missing fails", args{&oidc.AuthRequest{Scopes: []string{"openid"}, ResponseType: oidc.ResponseTypeCode}, mock.NewMockStorageExpectValidClientID(t), nil}, - true, + oidc.ErrInvalidRequest(), }, { "redirect_uri missing fails", args{&oidc.AuthRequest{Scopes: []string{"openid"}, ResponseType: oidc.ResponseTypeCode, ClientID: "client_id"}, mock.NewMockStorageExpectValidClientID(t), nil}, - true, + oidc.ErrInvalidRequest(), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := op.ValidateAuthRequest(nil, tt.args.authRequest, tt.args.storage, tt.args.verifier) - if (err != nil) != tt.wantErr { - t.Errorf("ValidateAuthRequest() error = %v, wantErr %v", err, tt.wantErr) + _, err := op.ValidateAuthRequest(context.TODO(), tt.args.authRequest, tt.args.storage, tt.args.verifier) + if tt.wantErr == nil && err != nil { + t.Errorf("ValidateAuthRequest() unexpected error = %v", err) } + if tt.wantErr != nil && !errors.Is(err, tt.wantErr) { + t.Errorf("ValidateAuthRequest() unexpected error = %v, want = %v", err, tt.wantErr) + } + }) + } +} + +func TestValidateAuthReqPrompt(t *testing.T) { + type args struct { + prompts []string + maxAge *uint + } + type res struct { + maxAge *uint + err error + } + tests := []struct { + name string + args args + res res + }{ + { + "no prompts and maxAge, ok", + args{ + nil, + nil, + }, + res{ + nil, + nil, + }, + }, + { + "no prompts but maxAge, ok", + args{ + nil, + oidc.NewMaxAge(10), + }, + res{ + oidc.NewMaxAge(10), + nil, + }, + }, + { + "prompt none, ok", + args{ + []string{"none"}, + oidc.NewMaxAge(10), + }, + res{ + oidc.NewMaxAge(10), + nil, + }, + }, + { + "prompt none with others, err", + args{ + []string{"none", "login"}, + oidc.NewMaxAge(10), + }, + res{ + nil, + oidc.ErrInvalidRequest(), + }, + }, + { + "prompt login, ok", + args{ + []string{"login"}, + nil, + }, + res{ + oidc.NewMaxAge(0), + nil, + }, + }, + { + "prompt login with maxAge, ok", + args{ + []string{"login"}, + oidc.NewMaxAge(10), + }, + res{ + oidc.NewMaxAge(0), + nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + maxAge, err := op.ValidateAuthReqPrompt(tt.args.prompts, tt.args.maxAge) + if tt.res.err == nil && err != nil { + t.Errorf("ValidateAuthRequest() unexpected error = %v", err) + } + if tt.res.err != nil && !errors.Is(err, tt.res.err) { + t.Errorf("ValidateAuthRequest() unexpected error = %v, want = %v", err, tt.res.err) + } + assert.Equal(t, tt.res.maxAge, maxAge) }) } } @@ -465,6 +561,80 @@ func TestValidateAuthReqRedirectURI(t *testing.T) { } } +func TestLoopbackOrLocalhost(t *testing.T) { + type args struct { + url string + } + tests := []struct { + name string + args args + want bool + }{ + { + "not parsable, false", + args{url: string('\n')}, + false, + }, + { + "not http, false", + args{url: "localhost/test"}, + false, + }, + { + "not http, false", + args{url: "http://localhost.com/test"}, + false, + }, + { + "v4 no port ok", + args{url: "http://127.0.0.1/test"}, + true, + }, + { + "v6 short no port ok", + args{url: "http://[::1]/test"}, + true, + }, + { + "v6 long no port ok", + args{url: "http://[0:0:0:0:0:0:0:1]/test"}, + true, + }, + { + "locahost no port ok", + args{url: "http://localhost/test"}, + true, + }, + { + "v4 with port ok", + args{url: "http://127.0.0.1:4200/test"}, + true, + }, + { + "v6 short with port ok", + args{url: "http://[::1]:4200/test"}, + true, + }, + { + "v6 long with port ok", + args{url: "http://[0:0:0:0:0:0:0:1]:4200/test"}, + true, + }, + { + "localhost with port ok", + args{url: "http://localhost:4200/test"}, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, got := op.HTTPLoopbackOrLocalhost(tt.args.url); got != tt.want { + t.Errorf("loopbackOrLocalhost() = %v, want %v", got, tt.want) + } + }) + } +} + func TestValidateAuthReqResponseType(t *testing.T) { type args struct { responseType oidc.ResponseType @@ -534,100 +704,122 @@ func TestRedirectToLogin(t *testing.T) { } } -func TestAuthorizeCallback(t *testing.T) { +func TestAuthResponseURL(t *testing.T) { type args struct { - w http.ResponseWriter - r *http.Request - authorizer op.Authorizer + redirectURI string + responseType oidc.ResponseType + responseMode oidc.ResponseMode + response interface{} + encoder httphelper.Encoder } - tests := []struct { - name string - args args - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - op.AuthorizeCallback(tt.args.w, tt.args.r, tt.args.authorizer) - }) - } -} - -func TestAuthResponse(t *testing.T) { - type args struct { - authReq op.AuthRequest - authorizer op.Authorizer - w http.ResponseWriter - r *http.Request - } - tests := []struct { - name string - args args - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - op.AuthResponse(tt.args.authReq, tt.args.authorizer, tt.args.w, tt.args.r) - }) - } -} - -func Test_LoopbackOrLocalhost(t *testing.T) { - type args struct { + type res struct { url string + err error } tests := []struct { name string args args - want bool + res res }{ { - "v4 no port ok", - args{url: "http://127.0.0.1/test"}, - true, + "encoding error", + args{ + "uri", + oidc.ResponseTypeCode, + "", + map[string]interface{}{"test": "test"}, + &mockEncoder{ + errors.New("error encoding"), + }, + }, + res{ + "", + oidc.ErrServerError(), + }, }, { - "v6 short no port ok", - args{url: "http://[::1]/test"}, - true, + "response mode query", + args{ + "uri", + oidc.ResponseTypeIDToken, + oidc.ResponseModeQuery, + map[string][]string{"test": {"test"}}, + &mockEncoder{}, + }, + res{ + "uri?test=test", + nil, + }, }, { - "v6 long no port ok", - args{url: "http://[0:0:0:0:0:0:0:1]/test"}, - true, + "response mode fragment", + args{ + "uri", + oidc.ResponseTypeCode, + oidc.ResponseModeFragment, + map[string][]string{"test": {"test"}}, + &mockEncoder{}, + }, + res{ + "uri#test=test", + nil, + }, }, { - "locahost no port ok", - args{url: "http://localhost/test"}, - true, + "response type code", + args{ + "uri", + oidc.ResponseTypeCode, + "", + map[string][]string{"test": {"test"}}, + &mockEncoder{}, + }, + res{ + "uri?test=test", + nil, + }, }, { - "v4 with port ok", - args{url: "http://127.0.0.1:4200/test"}, - true, - }, - { - "v6 short with port ok", - args{url: "http://[::1]:4200/test"}, - true, - }, - { - "v6 long with port ok", - args{url: "http://[0:0:0:0:0:0:0:1]:4200/test"}, - true, - }, - { - "localhost with port ok", - args{url: "http://localhost:4200/test"}, - true, + "response type id token", + args{ + "uri", + oidc.ResponseTypeIDToken, + "", + map[string][]string{"test": {"test"}}, + &mockEncoder{}, + }, + res{ + "uri#test=test", + nil, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if _, got := op.HTTPLoopbackOrLocalhost(tt.args.url); got != tt.want { - t.Errorf("loopbackOrLocalhost() = %v, want %v", got, tt.want) + got, err := op.AuthResponseURL(tt.args.redirectURI, tt.args.responseType, tt.args.responseMode, tt.args.response, tt.args.encoder) + if tt.res.err == nil && err != nil { + t.Errorf("ValidateAuthRequest() unexpected error = %v", err) + } + if tt.res.err != nil && !errors.Is(err, tt.res.err) { + t.Errorf("ValidateAuthRequest() unexpected error = %v, want = %v", err, tt.res.err) + } + if got != tt.res.url { + t.Errorf("AuthResponseURL() got = %v, want %v", got, tt.res.url) } }) } } + +type mockEncoder struct { + err error +} + +func (m *mockEncoder) Encode(src interface{}, dst map[string][]string) error { + if m.err != nil { + return m.err + } + for s, strings := range src.(map[string][]string) { + dst[s] = strings + } + return nil +} diff --git a/pkg/op/config.go b/pkg/op/config.go index 39c84c8..527e134 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -16,15 +16,23 @@ type Configuration interface { TokenEndpoint() Endpoint IntrospectionEndpoint() Endpoint UserinfoEndpoint() Endpoint + RevocationEndpoint() Endpoint EndSessionEndpoint() Endpoint KeysEndpoint() Endpoint AuthMethodPostSupported() bool CodeMethodS256Supported() bool AuthMethodPrivateKeyJWTSupported() bool + TokenEndpointSigningAlgorithmsSupported() []string GrantTypeRefreshTokenSupported() bool GrantTypeTokenExchangeSupported() bool GrantTypeJWTAuthorizationSupported() bool + IntrospectionAuthMethodPrivateKeyJWTSupported() bool + IntrospectionEndpointSigningAlgorithmsSupported() []string + RevocationAuthMethodPrivateKeyJWTSupported() bool + RevocationEndpointSigningAlgorithmsSupported() []string + RequestObjectSupported() bool + RequestObjectSigningAlgorithmsSupported() []string SupportedUILocales() []language.Tag } diff --git a/pkg/op/config_test.go b/pkg/op/config_test.go index e140074..5029df8 100644 --- a/pkg/op/config_test.go +++ b/pkg/op/config_test.go @@ -61,6 +61,7 @@ func TestValidateIssuer(t *testing.T) { }, } //ensure env is not set + //nolint:errcheck os.Unsetenv(OidcDevMode) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -86,6 +87,7 @@ func TestValidateIssuerDevLocalAllowed(t *testing.T) { false, }, } + //nolint:errcheck os.Setenv(OidcDevMode, "true") for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/op/crypto.go b/pkg/op/crypto.go index e95157d..e9dd67b 100644 --- a/pkg/op/crypto.go +++ b/pkg/op/crypto.go @@ -1,7 +1,7 @@ package op import ( - "github.com/caos/oidc/pkg/utils" + "github.com/caos/oidc/pkg/crypto" ) type Crypto interface { @@ -18,9 +18,9 @@ func NewAESCrypto(key [32]byte) Crypto { } func (c *aesCrypto) Encrypt(s string) (string, error) { - return utils.EncryptAES(s, c.key) + return crypto.EncryptAES(s, c.key) } func (c *aesCrypto) Decrypt(s string) (string, error) { - return utils.DecryptAES(s, c.key) + return crypto.DecryptAES(s, c.key) } diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 807aa20..955d0fa 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -3,8 +3,8 @@ package op import ( "net/http" + httphelper "github.com/caos/oidc/pkg/http" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" ) func discoveryHandler(c Configuration, s Signer) func(http.ResponseWriter, *http.Request) { @@ -14,28 +14,35 @@ func discoveryHandler(c Configuration, s Signer) func(http.ResponseWriter, *http } func Discover(w http.ResponseWriter, config *oidc.DiscoveryConfiguration) { - utils.MarshalJSON(w, config) + httphelper.MarshalJSON(w, config) } func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfiguration { return &oidc.DiscoveryConfiguration{ - Issuer: c.Issuer(), - AuthorizationEndpoint: c.AuthorizationEndpoint().Absolute(c.Issuer()), - TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()), - IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()), - UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()), - EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), - JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), - ScopesSupported: Scopes(c), - ResponseTypesSupported: ResponseTypes(c), - GrantTypesSupported: GrantTypes(c), - SubjectTypesSupported: SubjectTypes(c), - IDTokenSigningAlgValuesSupported: SigAlgorithms(s), - TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(c), - IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(c), - ClaimsSupported: SupportedClaims(c), - CodeChallengeMethodsSupported: CodeChallengeMethods(c), - UILocalesSupported: c.SupportedUILocales(), + Issuer: c.Issuer(), + AuthorizationEndpoint: c.AuthorizationEndpoint().Absolute(c.Issuer()), + TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()), + IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()), + UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()), + RevocationEndpoint: c.RevocationEndpoint().Absolute(c.Issuer()), + EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), + JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), + ScopesSupported: Scopes(c), + ResponseTypesSupported: ResponseTypes(c), + GrantTypesSupported: GrantTypes(c), + SubjectTypesSupported: SubjectTypes(c), + IDTokenSigningAlgValuesSupported: SigAlgorithms(s), + RequestObjectSigningAlgValuesSupported: RequestObjectSigAlgorithms(c), + TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(c), + TokenEndpointAuthSigningAlgValuesSupported: TokenSigAlgorithms(c), + IntrospectionEndpointAuthSigningAlgValuesSupported: IntrospectionSigAlgorithms(c), + IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(c), + RevocationEndpointAuthSigningAlgValuesSupported: RevocationSigAlgorithms(c), + RevocationEndpointAuthMethodsSupported: AuthMethodsRevocationEndpoint(c), + ClaimsSupported: SupportedClaims(c), + CodeChallengeMethodsSupported: CodeChallengeMethods(c), + UILocalesSupported: c.SupportedUILocales(), + RequestParameterSupported: c.RequestObjectSupported(), } } @@ -45,6 +52,7 @@ var DefaultSupportedScopes = []string{ oidc.ScopeEmail, oidc.ScopePhone, oidc.ScopeAddress, + oidc.ScopeOfflineAccess, } func Scopes(c Configuration) []string { @@ -127,6 +135,13 @@ func AuthMethodsTokenEndpoint(c Configuration) []oidc.AuthMethod { return authMethods } +func TokenSigAlgorithms(c Configuration) []string { + if !c.AuthMethodPrivateKeyJWTSupported() { + return nil + } + return c.TokenEndpointSigningAlgorithmsSupported() +} + func AuthMethodsIntrospectionEndpoint(c Configuration) []oidc.AuthMethod { authMethods := []oidc.AuthMethod{ oidc.AuthMethodBasic, @@ -137,6 +152,20 @@ func AuthMethodsIntrospectionEndpoint(c Configuration) []oidc.AuthMethod { return authMethods } +func AuthMethodsRevocationEndpoint(c Configuration) []oidc.AuthMethod { + authMethods := []oidc.AuthMethod{ + oidc.AuthMethodNone, + oidc.AuthMethodBasic, + } + if c.AuthMethodPostSupported() { + authMethods = append(authMethods, oidc.AuthMethodPost) + } + if c.AuthMethodPrivateKeyJWTSupported() { + authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT) + } + return authMethods +} + func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod { codeMethods := make([]oidc.CodeChallengeMethod, 0, 1) if c.CodeMethodS256Supported() { @@ -144,3 +173,24 @@ func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod { } return codeMethods } + +func IntrospectionSigAlgorithms(c Configuration) []string { + if !c.IntrospectionAuthMethodPrivateKeyJWTSupported() { + return nil + } + return c.IntrospectionEndpointSigningAlgorithmsSupported() +} + +func RevocationSigAlgorithms(c Configuration) []string { + if !c.RevocationAuthMethodPrivateKeyJWTSupported() { + return nil + } + return c.RevocationEndpointSigningAlgorithmsSupported() +} + +func RequestObjectSigAlgorithms(c Configuration) []string { + if !c.RequestObjectSupported() { + return nil + } + return c.RequestObjectSigningAlgorithmsSupported() +} diff --git a/pkg/op/discovery_test.go b/pkg/op/discovery_test.go index 4d97a01..1f0663d 100644 --- a/pkg/op/discovery_test.go +++ b/pkg/op/discovery_test.go @@ -37,7 +37,10 @@ func TestDiscover(t *testing.T) { op.Discover(tt.args.w, tt.args.config) rec := tt.args.w.(*httptest.ResponseRecorder) require.Equal(t, http.StatusOK, rec.Code) - require.Equal(t, `{"issuer":"https://issuer.com","request_uri_parameter_supported":false}`, rec.Body.String()) + require.Equal(t, + `{"issuer":"https://issuer.com","request_uri_parameter_supported":false} +`, + rec.Body.String()) }) } } diff --git a/pkg/op/error.go b/pkg/op/error.go index 06935c9..ea8d368 100644 --- a/pkg/op/error.go +++ b/pkg/op/error.go @@ -1,105 +1,46 @@ package op import ( - "fmt" "net/http" + httphelper "github.com/caos/oidc/pkg/http" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" ) -const ( - InvalidRequest errorType = "invalid_request" - InvalidRequestURI errorType = "invalid_request_uri" - InteractionRequired errorType = "interaction_required" - ServerError errorType = "server_error" -) - -var ( - ErrInvalidRequest = func(description string) *OAuthError { - return &OAuthError{ - ErrorType: InvalidRequest, - Description: description, - } - } - ErrInvalidRequestRedirectURI = func(description string) *OAuthError { - return &OAuthError{ - ErrorType: InvalidRequestURI, - Description: description, - redirectDisabled: true, - } - } - ErrInteractionRequired = func(description string) *OAuthError { - return &OAuthError{ - ErrorType: InteractionRequired, - Description: description, - } - } - ErrServerError = func(description string) *OAuthError { - return &OAuthError{ - ErrorType: ServerError, - Description: description, - } - } -) - -type errorType string - type ErrAuthRequest interface { GetRedirectURI() string GetResponseType() oidc.ResponseType GetState() string } -func AuthRequestError(w http.ResponseWriter, r *http.Request, authReq ErrAuthRequest, err error, encoder utils.Encoder) { +func AuthRequestError(w http.ResponseWriter, r *http.Request, authReq ErrAuthRequest, err error, encoder httphelper.Encoder) { if authReq == nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - e, ok := err.(*OAuthError) - if !ok { - e = new(OAuthError) - e.ErrorType = ServerError - e.Description = err.Error() - } - e.State = authReq.GetState() - if authReq.GetRedirectURI() == "" || e.redirectDisabled { + e := oidc.DefaultToServerError(err, err.Error()) + if authReq.GetRedirectURI() == "" || e.IsRedirectDisabled() { http.Error(w, e.Description, http.StatusBadRequest) return } - params, err := utils.URLEncodeResponse(e, encoder) + e.State = authReq.GetState() + var responseMode oidc.ResponseMode + if rm, ok := authReq.(interface{ GetResponseMode() oidc.ResponseMode }); ok { + responseMode = rm.GetResponseMode() + } + url, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), responseMode, e, encoder) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - url := authReq.GetRedirectURI() - responseType := authReq.GetResponseType() - if responseType == "" || responseType == oidc.ResponseTypeCode { - url += "?" + params - } else { - url += "#" + params - } http.Redirect(w, r, url, http.StatusFound) } func RequestError(w http.ResponseWriter, r *http.Request, err error) { - e, ok := err.(*OAuthError) - if !ok { - e = new(OAuthError) - e.ErrorType = ServerError - e.Description = err.Error() + e := oidc.DefaultToServerError(err, err.Error()) + status := http.StatusBadRequest + if e.ErrorType == oidc.InvalidClient { + status = 401 } - w.WriteHeader(http.StatusBadRequest) - utils.MarshalJSON(w, e) -} - -type OAuthError struct { - ErrorType errorType `json:"error" schema:"error"` - Description string `json:"error_description,omitempty" schema:"error_description,omitempty"` - State string `json:"state,omitempty" schema:"state,omitempty"` - redirectDisabled bool `json:"-" schema:"-"` -} - -func (e *OAuthError) Error() string { - return fmt.Sprintf("%s: %s", e.ErrorType, e.Description) + httphelper.MarshalJSONWithStatus(w, e, status) } diff --git a/pkg/op/keys.go b/pkg/op/keys.go index c4b11d4..e637066 100644 --- a/pkg/op/keys.go +++ b/pkg/op/keys.go @@ -6,7 +6,7 @@ import ( "gopkg.in/square/go-jose.v2" - "github.com/caos/oidc/pkg/utils" + httphelper "github.com/caos/oidc/pkg/http" ) type KeyProvider interface { @@ -22,9 +22,8 @@ func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) { func Keys(w http.ResponseWriter, r *http.Request, k KeyProvider) { keySet, err := k.GetKeySet(r.Context()) if err != nil { - w.WriteHeader(http.StatusInternalServerError) - utils.MarshalJSON(w, err) + httphelper.MarshalJSONWithStatus(w, err, http.StatusInternalServerError) return } - utils.MarshalJSON(w, keySet) + httphelper.MarshalJSON(w, keySet) } diff --git a/pkg/op/keys_test.go b/pkg/op/keys_test.go new file mode 100644 index 0000000..bf60a3e --- /dev/null +++ b/pkg/op/keys_test.go @@ -0,0 +1,100 @@ +package op_test + +import ( + "crypto/rsa" + "math/big" + "net/http" + "net/http/httptest" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "gopkg.in/square/go-jose.v2" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/op" + "github.com/caos/oidc/pkg/op/mock" +) + +func TestKeys(t *testing.T) { + type args struct { + k op.KeyProvider + } + type res struct { + statusCode int + contentType string + body string + } + tests := []struct { + name string + args args + res res + }{ + { + name: "error", + args: args{ + k: func() op.KeyProvider { + m := mock.NewMockKeyProvider(gomock.NewController(t)) + m.EXPECT().GetKeySet(gomock.Any()).Return(nil, oidc.ErrServerError()) + return m + }(), + }, + res: res{ + statusCode: http.StatusInternalServerError, + contentType: "application/json", + body: `{"error":"server_error"} +`, + }, + }, + { + name: "empty list", + args: args{ + k: func() op.KeyProvider { + m := mock.NewMockKeyProvider(gomock.NewController(t)) + m.EXPECT().GetKeySet(gomock.Any()).Return(nil, nil) + return m + }(), + }, + res: res{ + statusCode: http.StatusOK, + contentType: "application/json", + }, + }, + { + name: "list", + args: args{ + k: func() op.KeyProvider { + m := mock.NewMockKeyProvider(gomock.NewController(t)) + m.EXPECT().GetKeySet(gomock.Any()).Return( + &jose.JSONWebKeySet{Keys: []jose.JSONWebKey{ + { + Key: &rsa.PublicKey{ + N: big.NewInt(1), + E: 1, + }, + KeyID: "id", + }, + }}, + nil, + ) + return m + }(), + }, + res: res{ + statusCode: http.StatusOK, + contentType: "application/json", + body: `{"keys":[{"kty":"RSA","kid":"id","n":"AQ","e":"AQ"}]} +`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := httptest.NewRecorder() + op.Keys(w, httptest.NewRequest("GET", "/keys", nil), tt.args.k) + assert.Equal(t, tt.res.statusCode, w.Result().StatusCode) + assert.Equal(t, tt.res.contentType, w.Header().Get("content-type")) + assert.Equal(t, tt.res.body, w.Body.String()) + }) + } +} diff --git a/pkg/op/mock/authorizer.mock.go b/pkg/op/mock/authorizer.mock.go index 69f6927..3c18022 100644 --- a/pkg/op/mock/authorizer.mock.go +++ b/pkg/op/mock/authorizer.mock.go @@ -7,8 +7,8 @@ package mock import ( reflect "reflect" + http "github.com/caos/oidc/pkg/http" op "github.com/caos/oidc/pkg/op" - utils "github.com/caos/oidc/pkg/utils" gomock "github.com/golang/mock/gomock" ) @@ -50,10 +50,10 @@ func (mr *MockAuthorizerMockRecorder) Crypto() *gomock.Call { } // Decoder mocks base method. -func (m *MockAuthorizer) Decoder() utils.Decoder { +func (m *MockAuthorizer) Decoder() http.Decoder { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Decoder") - ret0, _ := ret[0].(utils.Decoder) + ret0, _ := ret[0].(http.Decoder) return ret0 } @@ -64,10 +64,10 @@ func (mr *MockAuthorizerMockRecorder) Decoder() *gomock.Call { } // Encoder mocks base method. -func (m *MockAuthorizer) Encoder() utils.Encoder { +func (m *MockAuthorizer) Encoder() http.Encoder { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Encoder") - ret0, _ := ret[0].(utils.Encoder) + ret0, _ := ret[0].(http.Encoder) return ret0 } @@ -105,6 +105,20 @@ func (mr *MockAuthorizerMockRecorder) Issuer() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Issuer", reflect.TypeOf((*MockAuthorizer)(nil).Issuer)) } +// RequestObjectSupported mocks base method. +func (m *MockAuthorizer) RequestObjectSupported() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RequestObjectSupported") + ret0, _ := ret[0].(bool) + return ret0 +} + +// RequestObjectSupported indicates an expected call of RequestObjectSupported. +func (mr *MockAuthorizerMockRecorder) RequestObjectSupported() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestObjectSupported", reflect.TypeOf((*MockAuthorizer)(nil).RequestObjectSupported)) +} + // Signer mocks base method. func (m *MockAuthorizer) Signer() op.Signer { m.ctrl.T.Helper() diff --git a/pkg/op/mock/configuration.mock.go b/pkg/op/mock/configuration.mock.go index 01c2c8d..3eb4542 100644 --- a/pkg/op/mock/configuration.mock.go +++ b/pkg/op/mock/configuration.mock.go @@ -147,6 +147,20 @@ func (mr *MockConfigurationMockRecorder) GrantTypeTokenExchangeSupported() *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeTokenExchangeSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeTokenExchangeSupported)) } +// IntrospectionAuthMethodPrivateKeyJWTSupported mocks base method. +func (m *MockConfiguration) IntrospectionAuthMethodPrivateKeyJWTSupported() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IntrospectionAuthMethodPrivateKeyJWTSupported") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IntrospectionAuthMethodPrivateKeyJWTSupported indicates an expected call of IntrospectionAuthMethodPrivateKeyJWTSupported. +func (mr *MockConfigurationMockRecorder) IntrospectionAuthMethodPrivateKeyJWTSupported() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntrospectionAuthMethodPrivateKeyJWTSupported", reflect.TypeOf((*MockConfiguration)(nil).IntrospectionAuthMethodPrivateKeyJWTSupported)) +} + // IntrospectionEndpoint mocks base method. func (m *MockConfiguration) IntrospectionEndpoint() op.Endpoint { m.ctrl.T.Helper() @@ -161,6 +175,20 @@ func (mr *MockConfigurationMockRecorder) IntrospectionEndpoint() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntrospectionEndpoint", reflect.TypeOf((*MockConfiguration)(nil).IntrospectionEndpoint)) } +// IntrospectionEndpointSigningAlgorithmsSupported mocks base method. +func (m *MockConfiguration) IntrospectionEndpointSigningAlgorithmsSupported() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IntrospectionEndpointSigningAlgorithmsSupported") + ret0, _ := ret[0].([]string) + return ret0 +} + +// IntrospectionEndpointSigningAlgorithmsSupported indicates an expected call of IntrospectionEndpointSigningAlgorithmsSupported. +func (mr *MockConfigurationMockRecorder) IntrospectionEndpointSigningAlgorithmsSupported() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntrospectionEndpointSigningAlgorithmsSupported", reflect.TypeOf((*MockConfiguration)(nil).IntrospectionEndpointSigningAlgorithmsSupported)) +} + // Issuer mocks base method. func (m *MockConfiguration) Issuer() string { m.ctrl.T.Helper() @@ -189,6 +217,76 @@ func (mr *MockConfigurationMockRecorder) KeysEndpoint() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeysEndpoint", reflect.TypeOf((*MockConfiguration)(nil).KeysEndpoint)) } +// RequestObjectSigningAlgorithmsSupported mocks base method. +func (m *MockConfiguration) RequestObjectSigningAlgorithmsSupported() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RequestObjectSigningAlgorithmsSupported") + ret0, _ := ret[0].([]string) + return ret0 +} + +// RequestObjectSigningAlgorithmsSupported indicates an expected call of RequestObjectSigningAlgorithmsSupported. +func (mr *MockConfigurationMockRecorder) RequestObjectSigningAlgorithmsSupported() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestObjectSigningAlgorithmsSupported", reflect.TypeOf((*MockConfiguration)(nil).RequestObjectSigningAlgorithmsSupported)) +} + +// RequestObjectSupported mocks base method. +func (m *MockConfiguration) RequestObjectSupported() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RequestObjectSupported") + ret0, _ := ret[0].(bool) + return ret0 +} + +// RequestObjectSupported indicates an expected call of RequestObjectSupported. +func (mr *MockConfigurationMockRecorder) RequestObjectSupported() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestObjectSupported", reflect.TypeOf((*MockConfiguration)(nil).RequestObjectSupported)) +} + +// RevocationAuthMethodPrivateKeyJWTSupported mocks base method. +func (m *MockConfiguration) RevocationAuthMethodPrivateKeyJWTSupported() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RevocationAuthMethodPrivateKeyJWTSupported") + ret0, _ := ret[0].(bool) + return ret0 +} + +// RevocationAuthMethodPrivateKeyJWTSupported indicates an expected call of RevocationAuthMethodPrivateKeyJWTSupported. +func (mr *MockConfigurationMockRecorder) RevocationAuthMethodPrivateKeyJWTSupported() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevocationAuthMethodPrivateKeyJWTSupported", reflect.TypeOf((*MockConfiguration)(nil).RevocationAuthMethodPrivateKeyJWTSupported)) +} + +// RevocationEndpoint mocks base method. +func (m *MockConfiguration) RevocationEndpoint() op.Endpoint { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RevocationEndpoint") + ret0, _ := ret[0].(op.Endpoint) + return ret0 +} + +// RevocationEndpoint indicates an expected call of RevocationEndpoint. +func (mr *MockConfigurationMockRecorder) RevocationEndpoint() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevocationEndpoint", reflect.TypeOf((*MockConfiguration)(nil).RevocationEndpoint)) +} + +// RevocationEndpointSigningAlgorithmsSupported mocks base method. +func (m *MockConfiguration) RevocationEndpointSigningAlgorithmsSupported() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RevocationEndpointSigningAlgorithmsSupported") + ret0, _ := ret[0].([]string) + return ret0 +} + +// RevocationEndpointSigningAlgorithmsSupported indicates an expected call of RevocationEndpointSigningAlgorithmsSupported. +func (mr *MockConfigurationMockRecorder) RevocationEndpointSigningAlgorithmsSupported() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevocationEndpointSigningAlgorithmsSupported", reflect.TypeOf((*MockConfiguration)(nil).RevocationEndpointSigningAlgorithmsSupported)) +} + // SupportedUILocales mocks base method. func (m *MockConfiguration) SupportedUILocales() []language.Tag { m.ctrl.T.Helper() @@ -217,6 +315,20 @@ func (mr *MockConfigurationMockRecorder) TokenEndpoint() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TokenEndpoint", reflect.TypeOf((*MockConfiguration)(nil).TokenEndpoint)) } +// TokenEndpointSigningAlgorithmsSupported mocks base method. +func (m *MockConfiguration) TokenEndpointSigningAlgorithmsSupported() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TokenEndpointSigningAlgorithmsSupported") + ret0, _ := ret[0].([]string) + return ret0 +} + +// TokenEndpointSigningAlgorithmsSupported indicates an expected call of TokenEndpointSigningAlgorithmsSupported. +func (mr *MockConfigurationMockRecorder) TokenEndpointSigningAlgorithmsSupported() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TokenEndpointSigningAlgorithmsSupported", reflect.TypeOf((*MockConfiguration)(nil).TokenEndpointSigningAlgorithmsSupported)) +} + // UserinfoEndpoint mocks base method. func (m *MockConfiguration) UserinfoEndpoint() op.Endpoint { m.ctrl.T.Helper() diff --git a/pkg/op/mock/generate.go b/pkg/op/mock/generate.go index beb3132..4dd020e 100644 --- a/pkg/op/mock/generate.go +++ b/pkg/op/mock/generate.go @@ -5,3 +5,4 @@ package mock //go:generate mockgen -package mock -destination ./client.mock.go github.com/caos/oidc/pkg/op Client //go:generate mockgen -package mock -destination ./configuration.mock.go github.com/caos/oidc/pkg/op Configuration //go:generate mockgen -package mock -destination ./signer.mock.go github.com/caos/oidc/pkg/op Signer +//go:generate mockgen -package mock -destination ./key.mock.go github.com/caos/oidc/pkg/op KeyProvider diff --git a/pkg/op/mock/key.mock.go b/pkg/op/mock/key.mock.go new file mode 100644 index 0000000..37e0677 --- /dev/null +++ b/pkg/op/mock/key.mock.go @@ -0,0 +1,51 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/caos/oidc/pkg/op (interfaces: KeyProvider) + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + jose "gopkg.in/square/go-jose.v2" +) + +// MockKeyProvider is a mock of KeyProvider interface. +type MockKeyProvider struct { + ctrl *gomock.Controller + recorder *MockKeyProviderMockRecorder +} + +// MockKeyProviderMockRecorder is the mock recorder for MockKeyProvider. +type MockKeyProviderMockRecorder struct { + mock *MockKeyProvider +} + +// NewMockKeyProvider creates a new mock instance. +func NewMockKeyProvider(ctrl *gomock.Controller) *MockKeyProvider { + mock := &MockKeyProvider{ctrl: ctrl} + mock.recorder = &MockKeyProviderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockKeyProvider) EXPECT() *MockKeyProviderMockRecorder { + return m.recorder +} + +// GetKeySet mocks base method. +func (m *MockKeyProvider) GetKeySet(arg0 context.Context) (*jose.JSONWebKeySet, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetKeySet", arg0) + ret0, _ := ret[0].(*jose.JSONWebKeySet) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetKeySet indicates an expected call of GetKeySet. +func (mr *MockKeyProviderMockRecorder) GetKeySet(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeySet", reflect.TypeOf((*MockKeyProvider)(nil).GetKeySet), arg0) +} diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index 4b44f2b..0763230 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -230,6 +230,20 @@ func (mr *MockStorageMockRecorder) Health(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockStorage)(nil).Health), arg0) } +// RevokeToken mocks base method. +func (m *MockStorage) RevokeToken(arg0 context.Context, arg1, arg2, arg3 string) *oidc.Error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RevokeToken", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(*oidc.Error) + return ret0 +} + +// RevokeToken indicates an expected call of RevokeToken. +func (mr *MockStorageMockRecorder) RevokeToken(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeToken", reflect.TypeOf((*MockStorage)(nil).RevokeToken), arg0, arg1, arg2, arg3) +} + // SaveAuthCode mocks base method. func (m *MockStorage) SaveAuthCode(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() diff --git a/pkg/op/op.go b/pkg/op/op.go index 3841227..8a5be26 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -12,8 +12,8 @@ import ( "golang.org/x/text/language" "gopkg.in/square/go-jose.v2" + httphelper "github.com/caos/oidc/pkg/http" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" ) const ( @@ -23,6 +23,7 @@ const ( defaultTokenEndpoint = "oauth/token" defaultIntrospectEndpoint = "oauth/introspect" defaultUserinfoEndpoint = "userinfo" + defaultRevocationEndpoint = "revoke" defaultEndSessionEndpoint = "end_session" defaultKeysEndpoint = "keys" ) @@ -33,6 +34,7 @@ var ( Token: NewEndpoint(defaultTokenEndpoint), Introspection: NewEndpoint(defaultIntrospectEndpoint), Userinfo: NewEndpoint(defaultUserinfoEndpoint), + Revocation: NewEndpoint(defaultRevocationEndpoint), EndSession: NewEndpoint(defaultEndSessionEndpoint), JwksURI: NewEndpoint(defaultKeysEndpoint), } @@ -41,8 +43,8 @@ var ( type OpenIDProvider interface { Configuration Storage() Storage - Decoder() utils.Decoder - Encoder() utils.Encoder + Decoder() httphelper.Decoder + Encoder() httphelper.Encoder IDTokenHintVerifier() IDTokenHintVerifier AccessTokenVerifier() AccessTokenVerifier Crypto() Crypto @@ -74,6 +76,7 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router router.Handle(o.TokenEndpoint().Relative(), intercept(tokenHandler(o))) router.HandleFunc(o.IntrospectionEndpoint().Relative(), introspectionHandler(o)) router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o)) + router.HandleFunc(o.RevocationEndpoint().Relative(), revocationHandler(o)) router.Handle(o.EndSessionEndpoint().Relative(), intercept(endSessionHandler(o))) router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o.Storage())) return router @@ -84,8 +87,10 @@ type Config struct { CryptoKey [32]byte DefaultLogoutRedirectURI string CodeMethodS256 bool + AuthMethodPost bool AuthMethodPrivateKeyJWT bool GrantTypeRefreshToken bool + RequestObjectSupported bool SupportedUILocales []language.Tag } @@ -94,6 +99,7 @@ type endpoints struct { Token Endpoint Introspection Endpoint Userinfo Endpoint + Revocation Endpoint EndSession Endpoint CheckSessionIframe Endpoint JwksURI Endpoint @@ -148,7 +154,6 @@ type openidProvider struct { decoder *schema.Decoder encoder *schema.Encoder interceptors []HttpInterceptor - retry func(int) (bool, int) timer <-chan time.Time } @@ -172,6 +177,10 @@ func (o *openidProvider) UserinfoEndpoint() Endpoint { return o.endpoints.Userinfo } +func (o *openidProvider) RevocationEndpoint() Endpoint { + return o.endpoints.Revocation +} + func (o *openidProvider) EndSessionEndpoint() Endpoint { return o.endpoints.EndSession } @@ -181,7 +190,7 @@ func (o *openidProvider) KeysEndpoint() Endpoint { } func (o *openidProvider) AuthMethodPostSupported() bool { - return true //todo: config + return o.config.AuthMethodPost } func (o *openidProvider) CodeMethodS256Supported() bool { @@ -192,6 +201,10 @@ func (o *openidProvider) AuthMethodPrivateKeyJWTSupported() bool { return o.config.AuthMethodPrivateKeyJWT } +func (o *openidProvider) TokenEndpointSigningAlgorithmsSupported() []string { + return []string{"RS256"} +} + func (o *openidProvider) GrantTypeRefreshTokenSupported() bool { return o.config.GrantTypeRefreshToken } @@ -204,6 +217,30 @@ func (o *openidProvider) GrantTypeJWTAuthorizationSupported() bool { return true } +func (o *openidProvider) IntrospectionAuthMethodPrivateKeyJWTSupported() bool { + return true +} + +func (o *openidProvider) IntrospectionEndpointSigningAlgorithmsSupported() []string { + return []string{"RS256"} +} + +func (o *openidProvider) RevocationAuthMethodPrivateKeyJWTSupported() bool { + return true +} + +func (o *openidProvider) RevocationEndpointSigningAlgorithmsSupported() []string { + return []string{"RS256"} +} + +func (o *openidProvider) RequestObjectSupported() bool { + return o.config.RequestObjectSupported +} + +func (o *openidProvider) RequestObjectSigningAlgorithmsSupported() []string { + return []string{"RS256"} +} + func (o *openidProvider) SupportedUILocales() []language.Tag { return o.config.SupportedUILocales } @@ -212,11 +249,11 @@ func (o *openidProvider) Storage() Storage { return o.storage } -func (o *openidProvider) Decoder() utils.Decoder { +func (o *openidProvider) Decoder() httphelper.Decoder { return o.decoder } -func (o *openidProvider) Encoder() utils.Encoder { +func (o *openidProvider) Encoder() httphelper.Encoder { return o.encoder } @@ -332,6 +369,16 @@ func WithCustomUserinfoEndpoint(endpoint Endpoint) Option { } } +func WithCustomRevocationEndpoint(endpoint Endpoint) Option { + return func(o *openidProvider) error { + if err := endpoint.Validate(); err != nil { + return err + } + o.endpoints.Revocation = endpoint + return nil + } +} + func WithCustomEndSessionEndpoint(endpoint Endpoint) Option { return func(o *openidProvider) error { if err := endpoint.Validate(); err != nil { @@ -352,11 +399,12 @@ func WithCustomKeysEndpoint(endpoint Endpoint) Option { } } -func WithCustomEndpoints(auth, token, userInfo, endSession, keys Endpoint) Option { +func WithCustomEndpoints(auth, token, userInfo, revocation, endSession, keys Endpoint) Option { return func(o *openidProvider) error { o.endpoints.Authorization = auth o.endpoints.Token = token o.endpoints.Userinfo = userInfo + o.endpoints.Revocation = revocation o.endpoints.EndSession = endSession o.endpoints.JwksURI = keys return nil diff --git a/pkg/op/probes.go b/pkg/op/probes.go index c6bb748..b6fdde2 100644 --- a/pkg/op/probes.go +++ b/pkg/op/probes.go @@ -5,7 +5,7 @@ import ( "errors" "net/http" - "github.com/caos/oidc/pkg/utils" + httphelper "github.com/caos/oidc/pkg/http" ) type ProbesFn func(context.Context) error @@ -49,7 +49,7 @@ func ReadyStorage(s Storage) ProbesFn { } func ok(w http.ResponseWriter) { - utils.MarshalJSON(w, status{"ok"}) + httphelper.MarshalJSON(w, status{"ok"}) } type status struct { diff --git a/pkg/op/session.go b/pkg/op/session.go index 4d75098..1f9290e 100644 --- a/pkg/op/session.go +++ b/pkg/op/session.go @@ -4,12 +4,12 @@ import ( "context" "net/http" + httphelper "github.com/caos/oidc/pkg/http" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" ) type SessionEnder interface { - Decoder() utils.Decoder + Decoder() httphelper.Decoder Storage() Storage IDTokenHintVerifier() IDTokenHintVerifier DefaultLogoutRedirectURI() string @@ -38,21 +38,21 @@ func EndSession(w http.ResponseWriter, r *http.Request, ender SessionEnder) { } err = ender.Storage().TerminateSession(r.Context(), session.UserID, clientID) if err != nil { - RequestError(w, r, ErrServerError("error terminating session")) + RequestError(w, r, oidc.DefaultToServerError(err, "error terminating session")) return } http.Redirect(w, r, session.RedirectURI, http.StatusFound) } -func ParseEndSessionRequest(r *http.Request, decoder utils.Decoder) (*oidc.EndSessionRequest, error) { +func ParseEndSessionRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.EndSessionRequest, error) { err := r.ParseForm() if err != nil { - return nil, ErrInvalidRequest("error parsing form") + return nil, oidc.ErrInvalidRequest().WithDescription("error parsing form").WithParent(err) } req := new(oidc.EndSessionRequest) err = decoder.Decode(req, r.Form) if err != nil { - return nil, ErrInvalidRequest("error decoding form") + return nil, oidc.ErrInvalidRequest().WithDescription("error decoding form").WithParent(err) } return req, nil } @@ -64,12 +64,12 @@ func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest, } claims, err := VerifyIDTokenHint(ctx, req.IdTokenHint, ender.IDTokenHintVerifier()) if err != nil { - return nil, ErrInvalidRequest("id_token_hint invalid") + return nil, oidc.ErrInvalidRequest().WithDescription("id_token_hint invalid").WithParent(err) } session.UserID = claims.GetSubject() session.Client, err = ender.Storage().GetClientByClientID(ctx, claims.GetAuthorizedParty()) if err != nil { - return nil, ErrServerError("") + return nil, oidc.DefaultToServerError(err, "") } if req.PostLogoutRedirectURI == "" { session.RedirectURI = ender.DefaultLogoutRedirectURI() @@ -81,5 +81,5 @@ func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest, return session, nil } } - return nil, ErrInvalidRequest("post_logout_redirect_uri invalid") + return nil, oidc.ErrInvalidRequest().WithDescription("post_logout_redirect_uri invalid") } diff --git a/pkg/op/storage.go b/pkg/op/storage.go index ca9ae7c..94c2a33 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -20,7 +20,8 @@ type AuthStorage interface { CreateAccessAndRefreshTokens(ctx context.Context, request TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (RefreshTokenRequest, error) - TerminateSession(context.Context, string, string) error + TerminateSession(ctx context.Context, userID string, clientID string) error + RevokeToken(ctx context.Context, token string, userID string, clientID string) *oidc.Error GetSigningKey(context.Context, chan<- jose.SigningKey) GetKeySet(context.Context) (*jose.JSONWebKeySet, error) diff --git a/pkg/op/token.go b/pkg/op/token.go index a587f8a..3e97360 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -4,8 +4,9 @@ import ( "context" "time" + "github.com/caos/oidc/pkg/crypto" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" + "github.com/caos/oidc/pkg/strings" ) type TokenCreator interface { @@ -64,7 +65,7 @@ func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storag func needsRefreshToken(tokenRequest TokenRequest, client Client) bool { switch req := tokenRequest.(type) { case AuthRequest: - return utils.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && req.GetResponseType() == oidc.ResponseTypeCode && ValidateGrantType(client, oidc.GrantTypeRefreshToken) + return strings.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && req.GetResponseType() == oidc.ResponseTypeCode && ValidateGrantType(client, oidc.GrantTypeRefreshToken) case RefreshTokenRequest: return true default: @@ -104,7 +105,7 @@ func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, ex } claims.SetPrivateClaims(privateClaims) } - return utils.Sign(claims, signer.Signer()) + return crypto.Sign(claims, signer.Signer()) } type IDTokenRequest interface { @@ -151,7 +152,7 @@ func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, v claims.SetCodeHash(codeHash) } - return utils.Sign(claims, signer.Signer()) + return crypto.Sign(claims, signer.Signer()) } func removeUserinfoScopes(scopes []string) []string { @@ -167,5 +168,5 @@ func removeUserinfoScopes(scopes []string) []string { newScopeList = append(newScopeList, scope) } } - return scopes + return newScopeList } diff --git a/pkg/op/token_code.go b/pkg/op/token_code.go index fa941df..7b5873c 100644 --- a/pkg/op/token_code.go +++ b/pkg/op/token_code.go @@ -2,11 +2,10 @@ package op import ( "context" - "errors" "net/http" + httphelper "github.com/caos/oidc/pkg/http" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" ) //CodeExchange handles the OAuth 2.0 authorization_code grant, including @@ -17,7 +16,7 @@ func CodeExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { RequestError(w, r, err) } if tokenReq.Code == "" { - RequestError(w, r, ErrInvalidRequest("code missing")) + RequestError(w, r, oidc.ErrInvalidRequest().WithDescription("code missing")) return } authReq, client, err := ValidateAccessTokenRequest(r.Context(), tokenReq, exchanger) @@ -30,11 +29,11 @@ func CodeExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { RequestError(w, r, err) return } - utils.MarshalJSON(w, resp) + httphelper.MarshalJSON(w, resp) } //ParseAccessTokenRequest parsed the http request into a oidc.AccessTokenRequest -func ParseAccessTokenRequest(r *http.Request, decoder utils.Decoder) (*oidc.AccessTokenRequest, error) { +func ParseAccessTokenRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.AccessTokenRequest, error) { request := new(oidc.AccessTokenRequest) err := ParseAuthenticatedTokenRequest(r, decoder, request) if err != nil { @@ -51,13 +50,13 @@ func ValidateAccessTokenRequest(ctx context.Context, tokenReq *oidc.AccessTokenR return nil, nil, err } if client.GetID() != authReq.GetClientID() { - return nil, nil, ErrInvalidRequest("invalid auth code") + return nil, nil, oidc.ErrInvalidGrant() } if !ValidateGrantType(client, oidc.GrantTypeCode) { - return nil, nil, ErrInvalidRequest("invalid_grant") + return nil, nil, oidc.ErrUnauthorizedClient() } if tokenReq.RedirectURI != authReq.GetRedirectURI() { - return nil, nil, ErrInvalidRequest("redirect_uri does not correspond") + return nil, nil, oidc.ErrInvalidGrant().WithDescription("redirect_uri does not correspond") } return authReq, client, nil } @@ -68,7 +67,7 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion { jwtExchanger, ok := exchanger.(JWTAuthorizationGrantExchanger) if !ok || !exchanger.AuthMethodPrivateKeyJWTSupported() { - return nil, nil, errors.New("auth_method private_key_jwt not supported") + return nil, nil, oidc.ErrInvalidClient().WithDescription("auth_method private_key_jwt not supported") } client, err = AuthorizePrivateJWTKey(ctx, tokenReq.ClientAssertion, jwtExchanger) if err != nil { @@ -79,10 +78,10 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, } client, err = exchanger.Storage().GetClientByClientID(ctx, tokenReq.ClientID) if err != nil { - return nil, nil, err + return nil, nil, oidc.ErrInvalidClient().WithParent(err) } if client.AuthMethod() == oidc.AuthMethodPrivateKeyJWT { - return nil, nil, errors.New("invalid_grant") + return nil, nil, oidc.ErrInvalidClient().WithDescription("private_key_jwt not allowed for this client") } if client.AuthMethod() == oidc.AuthMethodNone { request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code) @@ -93,9 +92,12 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, return request, client, err } if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() { - return nil, nil, errors.New("auth_method post not supported") + return nil, nil, oidc.ErrInvalidClient().WithDescription("auth_method post not supported") } err = AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, exchanger.Storage()) + if err != nil { + return nil, nil, err + } request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code) return request, client, err } @@ -104,7 +106,7 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, func AuthRequestByCode(ctx context.Context, storage Storage, code string) (AuthRequest, error) { authReq, err := storage.AuthRequestByCode(ctx, code) if err != nil { - return nil, ErrInvalidRequest("invalid code") + return nil, oidc.ErrInvalidGrant().WithDescription("invalid code").WithParent(err) } return authReq, nil } diff --git a/pkg/op/token_exchange.go b/pkg/op/token_exchange.go index 8d93e0c..501f6e5 100644 --- a/pkg/op/token_exchange.go +++ b/pkg/op/token_exchange.go @@ -3,28 +3,9 @@ package op import ( "errors" "net/http" - - "github.com/caos/oidc/pkg/oidc" ) //TokenExchange will handle the OAuth 2.0 token exchange grant ("urn:ietf:params:oauth:grant-type:token-exchange") func TokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { - tokenRequest, err := ParseTokenExchangeRequest(w, r) - if err != nil { - RequestError(w, r, err) - return - } - err = ValidateTokenExchangeRequest(tokenRequest, exchanger.Storage()) - if err != nil { - RequestError(w, r, err) - return - } -} - -func ParseTokenExchangeRequest(w http.ResponseWriter, r *http.Request) (oidc.TokenRequest, error) { - return nil, errors.New("Unimplemented") //TODO: impl -} - -func ValidateTokenExchangeRequest(tokenReq oidc.TokenRequest, storage Storage) error { - return errors.New("Unimplemented") //TODO: impl + RequestError(w, r, errors.New("unimplemented")) } diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go index e2ae0ad..8fd9187 100644 --- a/pkg/op/token_intospection.go +++ b/pkg/op/token_intospection.go @@ -5,12 +5,12 @@ import ( "net/http" "net/url" + httphelper "github.com/caos/oidc/pkg/http" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" ) type Introspector interface { - Decoder() utils.Decoder + Decoder() httphelper.Decoder Crypto() Crypto Storage() Storage AccessTokenVerifier() AccessTokenVerifier @@ -36,16 +36,16 @@ func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspecto } tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, token) if !ok { - utils.MarshalJSON(w, response) + httphelper.MarshalJSON(w, response) return } err = introspector.Storage().SetIntrospectionFromToken(r.Context(), response, tokenID, subject, clientID) if err != nil { - utils.MarshalJSON(w, response) + httphelper.MarshalJSON(w, response) return } response.SetActive(true) - utils.MarshalJSON(w, response) + httphelper.MarshalJSON(w, response) } func ParseTokenIntrospectionRequest(r *http.Request, introspector Introspector) (token, clientID string, err error) { diff --git a/pkg/op/token_jwt_profile.go b/pkg/op/token_jwt_profile.go index ac3e2a1..01a1411 100644 --- a/pkg/op/token_jwt_profile.go +++ b/pkg/op/token_jwt_profile.go @@ -5,8 +5,8 @@ import ( "net/http" "time" + httphelper "github.com/caos/oidc/pkg/http" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" ) type JWTAuthorizationGrantExchanger interface { @@ -37,18 +37,18 @@ func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizati RequestError(w, r, err) return } - utils.MarshalJSON(w, resp) + httphelper.MarshalJSON(w, resp) } -func ParseJWTProfileGrantRequest(r *http.Request, decoder utils.Decoder) (*oidc.JWTProfileGrantRequest, error) { +func ParseJWTProfileGrantRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.JWTProfileGrantRequest, error) { err := r.ParseForm() if err != nil { - return nil, ErrInvalidRequest("error parsing form") + return nil, oidc.ErrInvalidRequest().WithDescription("error parsing form").WithParent(err) } tokenReq := new(oidc.JWTProfileGrantRequest) err = decoder.Decode(tokenReq, r.Form) if err != nil { - return nil, ErrInvalidRequest("error decoding form") + return nil, oidc.ErrInvalidRequest().WithDescription("error decoding form").WithParent(err) } return tokenReq, nil } @@ -74,6 +74,6 @@ func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, crea //ParseJWTProfileRequest has been renamed to ParseJWTProfileGrantRequest // //deprecated: use ParseJWTProfileGrantRequest -func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (*oidc.JWTProfileGrantRequest, error) { +func ParseJWTProfileRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.JWTProfileGrantRequest, error) { return ParseJWTProfileGrantRequest(r, decoder) } diff --git a/pkg/op/token_refresh.go b/pkg/op/token_refresh.go index debcca1..0b6d470 100644 --- a/pkg/op/token_refresh.go +++ b/pkg/op/token_refresh.go @@ -6,8 +6,9 @@ import ( "net/http" "time" + httphelper "github.com/caos/oidc/pkg/http" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" + "github.com/caos/oidc/pkg/strings" ) type RefreshTokenRequest interface { @@ -37,11 +38,11 @@ func RefreshTokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exch RequestError(w, r, err) return } - utils.MarshalJSON(w, resp) + httphelper.MarshalJSON(w, resp) } //ParseRefreshTokenRequest parsed the http request into a oidc.RefreshTokenRequest -func ParseRefreshTokenRequest(r *http.Request, decoder utils.Decoder) (*oidc.RefreshTokenRequest, error) { +func ParseRefreshTokenRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.RefreshTokenRequest, error) { request := new(oidc.RefreshTokenRequest) err := ParseAuthenticatedTokenRequest(r, decoder, request) if err != nil { @@ -54,14 +55,14 @@ func ParseRefreshTokenRequest(r *http.Request, decoder utils.Decoder) (*oidc.Ref //and returns the data representing the original auth request corresponding to the refresh_token func ValidateRefreshTokenRequest(ctx context.Context, tokenReq *oidc.RefreshTokenRequest, exchanger Exchanger) (RefreshTokenRequest, Client, error) { if tokenReq.RefreshToken == "" { - return nil, nil, ErrInvalidRequest("code missing") + return nil, nil, oidc.ErrInvalidRequest().WithDescription("refresh_token missing") } request, client, err := AuthorizeRefreshClient(ctx, tokenReq, exchanger) if err != nil { return nil, nil, err } if client.GetID() != request.GetClientID() { - return nil, nil, ErrInvalidRequest("invalid auth code") + return nil, nil, oidc.ErrInvalidGrant() } if err = ValidateRefreshTokenScopes(tokenReq.Scopes, request); err != nil { return nil, nil, err @@ -77,15 +78,15 @@ func ValidateRefreshTokenScopes(requestedScopes []string, authRequest RefreshTok return nil } for _, scope := range requestedScopes { - if !utils.Contains(authRequest.GetScopes(), scope) { - return errors.New("invalid_scope") + if !strings.Contains(authRequest.GetScopes(), scope) { + return oidc.ErrInvalidScope() } } authRequest.SetCurrentScopes(requestedScopes) return nil } -//AuthorizeCodeClient checks the authorization of the client and that the used method was the one previously registered. +//AuthorizeRefreshClient checks the authorization of the client and that the used method was the one previously registered. //It than returns the data representing the original auth request corresponding to the refresh_token func AuthorizeRefreshClient(ctx context.Context, tokenReq *oidc.RefreshTokenRequest, exchanger Exchanger) (request RefreshTokenRequest, client Client, err error) { if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion { @@ -98,7 +99,7 @@ func AuthorizeRefreshClient(ctx context.Context, tokenReq *oidc.RefreshTokenRequ return nil, nil, err } if !ValidateGrantType(client, oidc.GrantTypeRefreshToken) { - return nil, nil, ErrInvalidRequest("invalid_grant") + return nil, nil, oidc.ErrUnauthorizedClient() } request, err = RefreshTokenRequestByRefreshToken(ctx, exchanger.Storage(), tokenReq.RefreshToken) return request, client, err @@ -108,17 +109,17 @@ func AuthorizeRefreshClient(ctx context.Context, tokenReq *oidc.RefreshTokenRequ return nil, nil, err } if !ValidateGrantType(client, oidc.GrantTypeRefreshToken) { - return nil, nil, ErrInvalidRequest("invalid_grant") + return nil, nil, oidc.ErrUnauthorizedClient() } if client.AuthMethod() == oidc.AuthMethodPrivateKeyJWT { - return nil, nil, errors.New("invalid_grant") + return nil, nil, oidc.ErrInvalidClient() } if client.AuthMethod() == oidc.AuthMethodNone { request, err = RefreshTokenRequestByRefreshToken(ctx, exchanger.Storage(), tokenReq.RefreshToken) return request, client, err } if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() { - return nil, nil, errors.New("auth_method post not supported") + return nil, nil, oidc.ErrInvalidClient().WithDescription("auth_method post not supported") } if err = AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, exchanger.Storage()); err != nil { return nil, nil, err @@ -132,7 +133,7 @@ func AuthorizeRefreshClient(ctx context.Context, tokenReq *oidc.RefreshTokenRequ func RefreshTokenRequestByRefreshToken(ctx context.Context, storage Storage, refreshToken string) (RefreshTokenRequest, error) { request, err := storage.TokenRequestByRefreshToken(ctx, refreshToken) if err != nil { - return nil, ErrInvalidRequest("invalid refreshToken") + return nil, oidc.ErrInvalidGrant().WithParent(err) } return request, nil } diff --git a/pkg/op/token_request.go b/pkg/op/token_request.go index fd26f19..6732bb1 100644 --- a/pkg/op/token_request.go +++ b/pkg/op/token_request.go @@ -5,14 +5,14 @@ import ( "net/http" "net/url" + httphelper "github.com/caos/oidc/pkg/http" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" ) type Exchanger interface { Issuer() string Storage() Storage - Decoder() utils.Decoder + Decoder() httphelper.Decoder Signer() Signer Crypto() Crypto AuthMethodPostSupported() bool @@ -24,7 +24,8 @@ type Exchanger interface { func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - switch r.FormValue("grant_type") { + grantType := r.FormValue("grant_type") + switch grantType { case string(oidc.GrantTypeCode): CodeExchange(w, r, exchanger) return @@ -44,14 +45,14 @@ func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Reque return } case "": - RequestError(w, r, ErrInvalidRequest("grant_type missing")) + RequestError(w, r, oidc.ErrInvalidRequest().WithDescription("grant_type missing")) return } - RequestError(w, r, ErrInvalidRequest("grant_type not supported")) + RequestError(w, r, oidc.ErrUnsupportedGrantType().WithDescription("%s not supported", grantType)) } } -//authenticatedTokenRequest is a helper interface for ParseAuthenticatedTokenRequest +//AuthenticatedTokenRequest is a helper interface for ParseAuthenticatedTokenRequest //it is implemented by oidc.AuthRequest and oidc.RefreshTokenRequest type AuthenticatedTokenRequest interface { SetClientID(string) @@ -60,48 +61,49 @@ type AuthenticatedTokenRequest interface { //ParseAuthenticatedTokenRequest parses the client_id and client_secret from the HTTP request from either //HTTP Basic Auth header or form body and sets them into the provided authenticatedTokenRequest interface -func ParseAuthenticatedTokenRequest(r *http.Request, decoder utils.Decoder, request AuthenticatedTokenRequest) error { +func ParseAuthenticatedTokenRequest(r *http.Request, decoder httphelper.Decoder, request AuthenticatedTokenRequest) error { err := r.ParseForm() if err != nil { - return ErrInvalidRequest("error parsing form") + return oidc.ErrInvalidRequest().WithDescription("error parsing form").WithParent(err) } err = decoder.Decode(request, r.Form) if err != nil { - return ErrInvalidRequest("error decoding form") + return oidc.ErrInvalidRequest().WithDescription("error decoding form").WithParent(err) } clientID, clientSecret, ok := r.BasicAuth() - if ok { - clientID, err = url.QueryUnescape(clientID) - if err != nil { - return ErrInvalidRequest("invalid basic auth header") - } - clientSecret, err = url.QueryUnescape(clientSecret) - if err != nil { - return ErrInvalidRequest("invalid basic auth header") - } - request.SetClientID(clientID) - request.SetClientSecret(clientSecret) + if !ok { + return nil } + clientID, err = url.QueryUnescape(clientID) + if err != nil { + return oidc.ErrInvalidClient().WithDescription("invalid basic auth header").WithParent(err) + } + clientSecret, err = url.QueryUnescape(clientSecret) + if err != nil { + return oidc.ErrInvalidClient().WithDescription("invalid basic auth header").WithParent(err) + } + request.SetClientID(clientID) + request.SetClientSecret(clientSecret) return nil } -//AuthorizeRefreshClientByClientIDSecret authorizes a client by validating the client_id and client_secret (Basic Auth and POST) +//AuthorizeClientIDSecret authorizes a client by validating the client_id and client_secret (Basic Auth and POST) func AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string, storage Storage) error { err := storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret) if err != nil { - return err //TODO: wrap? + return oidc.ErrInvalidClient().WithDescription("invalid client_id / client_secret").WithParent(err) } return nil } -//AuthorizeCodeClientByCodeChallenge authorizes a client by validating the code_verifier against the previously sent +//AuthorizeCodeChallenge authorizes a client by validating the code_verifier against the previously sent //code_challenge of the auth request (PKCE) func AuthorizeCodeChallenge(tokenReq *oidc.AccessTokenRequest, challenge *oidc.CodeChallenge) error { if tokenReq.CodeVerifier == "" { - return ErrInvalidRequest("code_challenge required") + return oidc.ErrInvalidRequest().WithDescription("code_challenge required") } if !oidc.VerifyCodeChallenge(challenge, tokenReq.CodeVerifier) { - return ErrInvalidRequest("code_challenge invalid") + return oidc.ErrInvalidGrant().WithDescription("invalid code challenge") } return nil } @@ -118,7 +120,7 @@ func AuthorizePrivateJWTKey(ctx context.Context, clientAssertion string, exchang return nil, err } if client.AuthMethod() != oidc.AuthMethodPrivateKeyJWT { - return nil, ErrInvalidRequest("invalid_client") + return nil, oidc.ErrInvalidClient() } return client, nil } diff --git a/pkg/op/token_revocation.go b/pkg/op/token_revocation.go new file mode 100644 index 0000000..fbaf8b7 --- /dev/null +++ b/pkg/op/token_revocation.go @@ -0,0 +1,136 @@ +package op + +import ( + "context" + "net/http" + "net/url" + "strings" + + httphelper "github.com/caos/oidc/pkg/http" + "github.com/caos/oidc/pkg/oidc" +) + +type Revoker interface { + Decoder() httphelper.Decoder + Crypto() Crypto + Storage() Storage + AccessTokenVerifier() AccessTokenVerifier + AuthMethodPrivateKeyJWTSupported() bool + AuthMethodPostSupported() bool +} + +type RevokerJWTProfile interface { + Revoker + JWTProfileVerifier() JWTProfileVerifier +} + +func revocationHandler(revoker Revoker) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + Revoke(w, r, revoker) + } +} + +func Revoke(w http.ResponseWriter, r *http.Request, revoker Revoker) { + token, _, clientID, err := ParseTokenRevocationRequest(r, revoker) + if err != nil { + RevocationRequestError(w, r, err) + return + } + tokenID, subject, ok := getTokenIDAndSubjectForRevocation(r.Context(), revoker, token) + if ok { + token = tokenID + } + if err := revoker.Storage().RevokeToken(r.Context(), token, subject, clientID); err != nil { + RevocationRequestError(w, r, err) + return + } + httphelper.MarshalJSON(w, nil) +} + +func ParseTokenRevocationRequest(r *http.Request, revoker Revoker) (token, tokenTypeHint, clientID string, err error) { + err = r.ParseForm() + if err != nil { + return "", "", "", oidc.ErrInvalidRequest().WithDescription("unable to parse request").WithParent(err) + } + req := new(struct { + oidc.RevocationRequest + oidc.ClientAssertionParams //for auth_method private_key_jwt + ClientID string `schema:"client_id"` //for auth_method none and post + ClientSecret string `schema:"client_secret"` //for auth_method post + }) + err = revoker.Decoder().Decode(req, r.Form) + if err != nil { + return "", "", "", oidc.ErrInvalidRequest().WithDescription("error decoding form").WithParent(err) + } + if req.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion { + revokerJWTProfile, ok := revoker.(RevokerJWTProfile) + if !ok || !revoker.AuthMethodPrivateKeyJWTSupported() { + return "", "", "", oidc.ErrInvalidClient().WithDescription("auth_method private_key_jwt not supported") + } + profile, err := VerifyJWTAssertion(r.Context(), req.ClientAssertion, revokerJWTProfile.JWTProfileVerifier()) + if err == nil { + return req.Token, req.TokenTypeHint, profile.Issuer, nil + } + return "", "", "", err + } + clientID, clientSecret, ok := r.BasicAuth() + if ok { + clientID, err = url.QueryUnescape(clientID) + if err != nil { + return "", "", "", oidc.ErrInvalidClient().WithDescription("invalid basic auth header").WithParent(err) + } + clientSecret, err = url.QueryUnescape(clientSecret) + if err != nil { + return "", "", "", oidc.ErrInvalidClient().WithDescription("invalid basic auth header").WithParent(err) + } + if err = AuthorizeClientIDSecret(r.Context(), clientID, clientSecret, revoker.Storage()); err != nil { + return "", "", "", err + } + return req.Token, req.TokenTypeHint, clientID, nil + } + if req.ClientID == "" { + return "", "", "", oidc.ErrInvalidClient().WithDescription("invalid authorization") + } + client, err := revoker.Storage().GetClientByClientID(r.Context(), req.ClientID) + if err != nil { + return "", "", "", oidc.ErrInvalidClient().WithParent(err) + } + if req.ClientSecret == "" { + if client.AuthMethod() != oidc.AuthMethodNone { + return "", "", "", oidc.ErrInvalidClient().WithDescription("invalid authorization") + } + return req.Token, req.TokenTypeHint, req.ClientID, nil + } + if client.AuthMethod() == oidc.AuthMethodPost && !revoker.AuthMethodPostSupported() { + return "", "", "", oidc.ErrInvalidClient().WithDescription("auth_method post not supported") + } + if err = AuthorizeClientIDSecret(r.Context(), req.ClientID, req.ClientSecret, revoker.Storage()); err != nil { + return "", "", "", err + } + return req.Token, req.TokenTypeHint, req.ClientID, nil +} + +func RevocationRequestError(w http.ResponseWriter, r *http.Request, err error) { + e := oidc.DefaultToServerError(err, err.Error()) + status := http.StatusBadRequest + if e.ErrorType == oidc.InvalidClient { + status = 401 + } + httphelper.MarshalJSONWithStatus(w, e, status) +} + +func getTokenIDAndSubjectForRevocation(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, bool) { + tokenIDSubject, err := userinfoProvider.Crypto().Decrypt(accessToken) + if err == nil { + splitToken := strings.Split(tokenIDSubject, ":") + if len(splitToken) != 2 { + return "", "", false + } + return splitToken[0], splitToken[1], true + } + accessTokenClaims, err := VerifyAccessToken(ctx, accessToken, userinfoProvider.AccessTokenVerifier()) + if err != nil { + return "", "", false + } + return accessTokenClaims.GetTokenID(), accessTokenClaims.GetSubject(), true +} diff --git a/pkg/op/userinfo.go b/pkg/op/userinfo.go index 9abf378..f07a8bc 100644 --- a/pkg/op/userinfo.go +++ b/pkg/op/userinfo.go @@ -6,12 +6,12 @@ import ( "net/http" "strings" + httphelper "github.com/caos/oidc/pkg/http" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" ) type UserinfoProvider interface { - Decoder() utils.Decoder + Decoder() httphelper.Decoder Crypto() Crypto Storage() Storage AccessTokenVerifier() AccessTokenVerifier @@ -37,14 +37,13 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP info := oidc.NewUserInfo() err = userinfoProvider.Storage().SetUserinfoFromToken(r.Context(), info, tokenID, subject, r.Header.Get("origin")) if err != nil { - w.WriteHeader(http.StatusForbidden) - utils.MarshalJSON(w, err) + httphelper.MarshalJSONWithStatus(w, err, http.StatusForbidden) return } - utils.MarshalJSON(w, info) + httphelper.MarshalJSON(w, info) } -func ParseUserinfoRequest(r *http.Request, decoder utils.Decoder) (string, error) { +func ParseUserinfoRequest(r *http.Request, decoder httphelper.Decoder) (string, error) { accessToken, err := getAccessToken(r) if err == nil { return accessToken, nil diff --git a/pkg/op/verifier_access_token.go b/pkg/op/verifier_access_token.go index 05168a6..2220244 100644 --- a/pkg/op/verifier_access_token.go +++ b/pkg/op/verifier_access_token.go @@ -49,7 +49,7 @@ func (i *accessTokenVerifier) KeySet() oidc.KeySet { } func NewAccessTokenVerifier(issuer string, keySet oidc.KeySet) AccessTokenVerifier { - verifier := &idTokenHintVerifier{ + verifier := &accessTokenVerifier{ issuer: issuer, keySet: keySet, } diff --git a/pkg/utils/strings.go b/pkg/strings/strings.go similarity index 89% rename from pkg/utils/strings.go rename to pkg/strings/strings.go index 5ffcd37..af48cf3 100644 --- a/pkg/utils/strings.go +++ b/pkg/strings/strings.go @@ -1,4 +1,4 @@ -package utils +package strings func Contains(list []string, needle string) bool { for _, item := range list { diff --git a/pkg/utils/strings_test.go b/pkg/strings/strings_test.go similarity index 98% rename from pkg/utils/strings_test.go rename to pkg/strings/strings_test.go index 86af2af..78698d4 100644 --- a/pkg/utils/strings_test.go +++ b/pkg/strings/strings_test.go @@ -1,4 +1,4 @@ -package utils +package strings import "testing" From f103b56e9545d606aa5767cb72a2d690843c149b Mon Sep 17 00:00:00 2001 From: Rohinish <92542124+rohinish404@users.noreply.github.com> Date: Sat, 22 Jan 2022 23:50:58 +0530 Subject: [PATCH 086/502] docs(readme): corrected terminology --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eaed6a8..cbf680e 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,8 @@ CLIENT_ID=web CLIENT_SECRET=web ISSUER=http://localhost:9998/ SCOPES=openid PORT | | Code Flow | Implicit Flow | Hybrid Flow | Discovery | PKCE | Token Exchange | mTLS | JWT Profile | Refresh Token | |----------------|-----------|---------------|-------------|-----------|------|----------------|---------|-------------|---------------| -| Relaying Party | yes | no[^1] | no | yes | yes | partial | not yet | yes | yes | -| Origin Party | yes | yes | not yet | yes | yes | not yet | not yet | yes | yes | +| Relying Party | yes | no[^1] | no | yes | yes | partial | not yet | yes | yes | +| OpenID Provider | yes | yes | not yet | yes | yes | not yet | not yet | yes | yes | ### Resources From bcd9ec8d8525bc65c315a213683c6c41e71f32b1 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 28 Jan 2022 09:42:42 +0100 Subject: [PATCH 087/502] fix: handle keys without `use` in FindMatchingKey --- pkg/oidc/keyset.go | 28 ++++++++--- pkg/oidc/keyset_test.go | 109 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 8 deletions(-) diff --git a/pkg/oidc/keyset.go b/pkg/oidc/keyset.go index 3eca654..57fe526 100644 --- a/pkg/oidc/keyset.go +++ b/pkg/oidc/keyset.go @@ -53,7 +53,7 @@ func FindKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (jose.JSON return key, err == nil } -//FindMatchingKey searches the given JSON Web Keys for the requested key ID, usage and key type +//FindMatchingKey searches the given JSON Web Keys for the requested key ID, usage and alg type // //will return the key immediately if matches exact (id, usage, type) // @@ -61,15 +61,27 @@ func FindKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (jose.JSON func FindMatchingKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (key jose.JSONWebKey, err error) { var validKeys []jose.JSONWebKey for _, k := range keys { - if k.Use == use && algToKeyType(k.Key, expectedAlg) { - if k.KeyID == keyID && keyID != "" { - return k, nil - } - if k.KeyID == "" || keyID == "" { - validKeys = append(validKeys, k) - } + //ignore all keys with wrong use (let empty use of published key pass) + if k.Use != use && k.Use != "" { + continue + } + //ignore all keys with wrong algorithm type + if !algToKeyType(k.Key, expectedAlg) { + continue + } + //if we get here, use and alg match, so an equal (not empty) keyID is an exact match + if k.KeyID == keyID && keyID != "" { + return k, nil + } + //keyIDs did not match or at least one was empty (if later, then it could be a match) + if k.KeyID == "" || keyID == "" { + validKeys = append(validKeys, k) } } + //if we get here, no match was possible at all (use / alg) or no exact match due to + //the signed JWT and / or the published keys didn't have a kid + //if later applies and only one key could be found, we'll return it + //otherwise a corresponding error will be thrown if len(validKeys) == 1 { return validKeys[0], nil } diff --git a/pkg/oidc/keyset_test.go b/pkg/oidc/keyset_test.go index 802edec..1c8b369 100644 --- a/pkg/oidc/keyset_test.go +++ b/pkg/oidc/keyset_test.go @@ -139,6 +139,27 @@ func TestFindKey(t *testing.T) { err: nil, }, }, + { + "single key no use, jwt with kid, match", + args{ + keyID: "id", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + KeyID: "id", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + KeyID: "id", + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, { "single key wrong kid, ErrKeyNone", args{ @@ -304,6 +325,94 @@ func TestFindKey(t *testing.T) { err: nil, }, }, + { + "multiple keys, no use, jwt with kid, match", + args{ + keyID: "id1", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + KeyID: "id1", + Key: &rsa.PublicKey{}, + }, + { + KeyID: "id2", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + KeyID: "id1", + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, + { + "multiple keys, no use, jwt without kid, ErrKeyMultiple", + args{ + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + KeyID: "id1", + Key: &rsa.PublicKey{}, + }, + { + KeyID: "id2", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyMultiple, + }, + }, + { + "multiple keys, no use or id, jwt with kid, ErrKeyMultiple", + args{ + use: KeyUseSignature, + expectedAlg: "RS256", + keyID: "id1", + keys: []jose.JSONWebKey{ + { + Key: &rsa.PublicKey{}, + }, + { + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyMultiple, + }, + }, + { + "multiple keys (only one matching alg), jwt with kid, match", + args{ + use: KeyUseSignature, + expectedAlg: "RS256", + keyID: "id1", + keys: []jose.JSONWebKey{ + { + Key: &rsa.PublicKey{}, + }, + { + Key: &ecdsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 7ea5ddf2500545d3428084630bf28d7dbc0ff9be Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 28 Jan 2022 09:48:37 +0100 Subject: [PATCH 088/502] add missing import --- pkg/oidc/keyset_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/oidc/keyset_test.go b/pkg/oidc/keyset_test.go index 1c8b369..82b3ee8 100644 --- a/pkg/oidc/keyset_test.go +++ b/pkg/oidc/keyset_test.go @@ -1,6 +1,7 @@ package oidc import ( + "crypto/ecdsa" "crypto/rsa" "errors" "reflect" From e39146c98e7f4c801b377954d3d6da40d9e77c78 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 31 Jan 2022 07:27:52 +0100 Subject: [PATCH 089/502] fix: ensure signer has key on OP creation --- pkg/op/op.go | 2 +- pkg/op/signer.go | 36 +++++++++++++++++++++++------------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/pkg/op/op.go b/pkg/op/op.go index 8a5be26..1c233ea 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -125,8 +125,8 @@ func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opO } keyCh := make(chan jose.SigningKey) - o.signer = NewSigner(ctx, storage, keyCh) go storage.GetSigningKey(ctx, keyCh) + o.signer = NewSigner(ctx, storage, keyCh) o.httpHandler = CreateRouter(o, o.interceptors...) diff --git a/pkg/op/signer.go b/pkg/op/signer.go index d59ea8e..aaa24d0 100644 --- a/pkg/op/signer.go +++ b/pkg/op/signer.go @@ -25,6 +25,12 @@ func NewSigner(ctx context.Context, storage AuthStorage, keyCh <-chan jose.Signi storage: storage, } + select { + case <-ctx.Done(): + return nil + case key := <-keyCh: + s.exchangeSigningKey(key) + } go s.refreshSigningKey(ctx, keyCh) return s @@ -50,23 +56,27 @@ func (s *tokenSigner) refreshSigningKey(ctx context.Context, keyCh <-chan jose.S case <-ctx.Done(): return case key := <-keyCh: - s.alg = key.Algorithm - if key.Algorithm == "" || key.Key == nil { - s.signer = nil - logging.Log("OP-DAvt4").Warn("signer has no key") - continue - } - var err error - s.signer, err = jose.NewSigner(key, &jose.SignerOptions{}) - if err != nil { - logging.Log("OP-pf32aw").WithError(err).Error("error creating signer") - continue - } - logging.Log("OP-agRf2").Info("signer exchanged signing key") + s.exchangeSigningKey(key) } } } +func (s *tokenSigner) exchangeSigningKey(key jose.SigningKey) { + s.alg = key.Algorithm + if key.Algorithm == "" || key.Key == nil { + s.signer = nil + logging.Log("OP-DAvt4").Warn("signer has no key") + return + } + var err error + s.signer, err = jose.NewSigner(key, &jose.SignerOptions{}) + if err != nil { + logging.Log("OP-pf32aw").WithError(err).Error("error creating signer") + return + } + logging.Log("OP-agRf2").Info("signer exchanged signing key") +} + func (s *tokenSigner) SignatureAlgorithm() jose.SignatureAlgorithm { return s.alg } From 5601add62829840514aca8e2896d6d7282e601bb Mon Sep 17 00:00:00 2001 From: Ydris Rebibane Date: Wed, 16 Feb 2022 09:14:54 +0100 Subject: [PATCH 090/502] feat: Allow the use of a custom discovery endpoint (#152) * Allow the use of custom endpoints * Remove the custom constrtouctor and replace with an optional argument to override the discovery endpoit --- pkg/client/client.go | 7 ++++++- pkg/client/rp/relaying_party.go | 20 ++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index 1828d1d..ac6cd56 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -26,8 +26,13 @@ var ( ) //Discover calls the discovery endpoint of the provided issuer and returns its configuration -func Discover(issuer string, httpClient *http.Client) (*oidc.DiscoveryConfiguration, error) { +//It accepts an optional argument "wellknownUrl" which can be used to overide the dicovery endpoint url +func Discover(issuer string, httpClient *http.Client, wellKnownUrl ...string) (*oidc.DiscoveryConfiguration, error) { + wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint + if len(wellKnownUrl) == 1 && wellKnownUrl[0] != "" { + wellKnown = wellKnownUrl[0] + } req, err := http.NewRequest("GET", wellKnown, nil) if err != nil { return nil, err diff --git a/pkg/client/rp/relaying_party.go b/pkg/client/rp/relaying_party.go index 23c37fc..98ab354 100644 --- a/pkg/client/rp/relaying_party.go +++ b/pkg/client/rp/relaying_party.go @@ -69,11 +69,12 @@ var ( ) type relyingParty struct { - issuer string - endpoints Endpoints - oauthConfig *oauth2.Config - oauth2Only bool - pkce bool + issuer string + DiscoveryEndpoint string + endpoints Endpoints + oauthConfig *oauth2.Config + oauth2Only bool + pkce bool httpClient *http.Client cookieHandler *httphelper.CookieHandler @@ -170,7 +171,7 @@ func NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, sco return nil, err } } - discoveryConfiguration, err := client.Discover(rp.issuer, rp.httpClient) + discoveryConfiguration, err := client.Discover(rp.issuer, rp.httpClient, rp.DiscoveryEndpoint) if err != nil { return nil, err } @@ -184,6 +185,13 @@ func NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, sco //Option is the type for providing dynamic options to the relyingParty type Option func(*relyingParty) error +func WithCustomDiscoveryUrl(url string) Option { + return func(rp *relyingParty) error { + rp.DiscoveryEndpoint = url + return nil + } +} + //WithCookieHandler set a `CookieHandler` for securing the various redirects func WithCookieHandler(cookieHandler *httphelper.CookieHandler) Option { return func(rp *relyingParty) error { From b914990e15cf1910debf05bfeac6f16a128ad0b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Mar 2022 06:59:53 +0100 Subject: [PATCH 091/502] chore(deps): bump actions/checkout from 2 to 3 (#161) Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/release.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0101ea5..6bdfcdf 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 471c09e..c404ba3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: go: ['1.14', '1.15', '1.16', '1.17'] name: Go ${{ matrix.go }} test steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup go uses: actions/setup-go@v2 with: @@ -36,7 +36,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Source checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Semantic Release uses: cycjimmy/semantic-release-action@v2 with: From c07557be026cbef2e5373a37da54b12b01b5d098 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Wed, 16 Mar 2022 10:55:29 +0100 Subject: [PATCH 092/502] feat: build the redirect after a successful login with AuthCallbackURL function (#164) --- pkg/op/op.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pkg/op/op.go b/pkg/op/op.go index 1c233ea..e910bf6 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -19,6 +19,7 @@ import ( const ( healthEndpoint = "/healthz" readinessEndpoint = "/ready" + authCallbackPathSuffix = "/callback" defaultAuthorizationEndpoint = "authorize" defaultTokenEndpoint = "oauth/token" defaultIntrospectEndpoint = "oauth/introspect" @@ -72,7 +73,7 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router router.HandleFunc(readinessEndpoint, readyHandler(o.Probes())) router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Signer())) router.Handle(o.AuthorizationEndpoint().Relative(), intercept(authorizeHandler(o))) - router.NewRoute().Path(o.AuthorizationEndpoint().Relative()+"/callback").Queries("id", "{id}").Handler(intercept(authorizeCallbackHandler(o))) + router.NewRoute().Path(authCallbackPath(o)).Queries("id", "{id}").Handler(intercept(authorizeCallbackHandler(o))) router.Handle(o.TokenEndpoint().Relative(), intercept(tokenHandler(o))) router.HandleFunc(o.IntrospectionEndpoint().Relative(), introspectionHandler(o)) router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o)) @@ -82,6 +83,17 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router return router } +//AuthCallbackURL builds the url for the redirect (with the requestID) after a successful login +func AuthCallbackURL(o OpenIDProvider) func(string) string { + return func(requestID string) string { + return o.AuthorizationEndpoint().Absolute(o.Issuer()) + authCallbackPathSuffix + "?id=" + requestID + } +} + +func authCallbackPath(o OpenIDProvider) string { + return o.AuthorizationEndpoint().Relative() + authCallbackPathSuffix +} + type Config struct { Issuer string CryptoKey [32]byte From ab76b3518f03ade2b45b8477a61e2907ced996e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Mar 2022 11:14:57 +0100 Subject: [PATCH 093/502] chore(deps): bump github.com/caos/logging from 0.0.2 to 0.3.1 (#159) * chore(deps): bump github.com/caos/logging from 0.0.2 to 0.3.1 Bumps [github.com/caos/logging](https://github.com/caos/logging) from 0.0.2 to 0.3.1. - [Release notes](https://github.com/caos/logging/releases) - [Changelog](https://github.com/caos/logging/blob/master/.releaserc.js) - [Commits](https://github.com/caos/logging/compare/v0.0.2...v0.3.1) --- updated-dependencies: - dependency-name: github.com/caos/logging dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * update logging * update logging Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Livio Amstutz --- go.mod | 2 +- go.sum | 38 ++++++++------------------------------ pkg/op/signer.go | 6 +++--- 3 files changed, 12 insertions(+), 34 deletions(-) diff --git a/go.mod b/go.mod index a340df8..b2def36 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/caos/oidc go 1.15 require ( - github.com/caos/logging v0.0.2 + github.com/caos/logging v0.3.1 github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.2 // indirect github.com/google/go-github/v31 v31.0.0 diff --git a/go.sum b/go.sum index 4ff0c39..1c91e30 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/caos/logging v0.0.2 h1:ebg5C/HN0ludYR+WkvnFjwSExF4wvyiWPyWGcKMYsoo= -github.com/caos/logging v0.0.2/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0= +github.com/caos/logging v0.3.1 h1:892AMeHs09D0e3ZcGB+QDRsZ5+2xtPAsAhOy8eKfztc= +github.com/caos/logging v0.3.1/go.mod h1:B8QNS0WDmR2Keac52Fw+XN4ZJkzLDGrcRIPB2Ux4uRo= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -50,13 +50,9 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= 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= -github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= -github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -99,7 +95,6 @@ github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6C github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -125,40 +120,26 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so= -github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 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= @@ -253,17 +234,14 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -280,8 +258,9 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220207234003-57398862261d h1:Bm7BNOQt2Qv7ZqysjeLjgCBanX+88Z/OtdvsrEv1Djc= +golang.org/x/sys v0.0.0-20220207234003-57398862261d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -419,15 +398,14 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= -gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= -gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/op/signer.go b/pkg/op/signer.go index aaa24d0..0dabf67 100644 --- a/pkg/op/signer.go +++ b/pkg/op/signer.go @@ -65,16 +65,16 @@ func (s *tokenSigner) exchangeSigningKey(key jose.SigningKey) { s.alg = key.Algorithm if key.Algorithm == "" || key.Key == nil { s.signer = nil - logging.Log("OP-DAvt4").Warn("signer has no key") + logging.Warn("signer has no key") return } var err error s.signer, err = jose.NewSigner(key, &jose.SignerOptions{}) if err != nil { - logging.Log("OP-pf32aw").WithError(err).Error("error creating signer") + logging.New().WithError(err).Error("error creating signer") return } - logging.Log("OP-agRf2").Info("signer exchanged signing key") + logging.Info("signer exchanged signing key") } func (s *tokenSigner) SignatureAlgorithm() jose.SignatureAlgorithm { From d740fe17104f4a7346b44e5b37e47fc6b181d73f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Mar 2022 11:18:08 +0100 Subject: [PATCH 094/502] chore(deps): bump github.com/stretchr/testify from 1.7.0 to 1.7.1 (#163) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.0 to 1.7.1. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.7.0...v1.7.1) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index b2def36..f23a1b3 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/gorilla/securecookie v1.1.1 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/sirupsen/logrus v1.8.1 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.7.1 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 golang.org/x/text v0.3.7 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect diff --git a/go.sum b/go.sum index 1c91e30..605a76e 100644 --- a/go.sum +++ b/go.sum @@ -138,8 +138,9 @@ github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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= From 0dd79cb6f9cfda29f630f9fa28e000d10348c71b Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Tue, 22 Mar 2022 07:26:00 +0100 Subject: [PATCH 095/502] chore(build): add go 1.18 to matrix build (#166) * chore(build): add go 1.18 to matrix build * add 1.18 * Update README.md * Update release.yml --- .github/workflows/release.yml | 2 +- README.md | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c404ba3..c163975 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-18.04 strategy: matrix: - go: ['1.14', '1.15', '1.16', '1.17'] + go: ['1.15', '1.16', '1.17', '1.18'] name: Go ${{ matrix.go }} test steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index cbf680e..e841dad 100644 --- a/README.md +++ b/README.md @@ -52,13 +52,16 @@ For your convenience you can find the relevant standards linked below. ## Supported Go Versions +For security reasons, we only support and recommend the use of one of the latest two Go versions (:white_check_mark:). +Versions that also build are marked with :warning:. + | Version | Supported | |---------|--------------------| -| <1.13 | :x: | -| 1.14 | :white_check_mark: | -| 1.15 | :white_check_mark: | -| 1.16 | :white_check_mark: | +| <1.15 | :x: | +| 1.15 | :warning: | +| 1.16 | :warning: | | 1.17 | :white_check_mark: | +| 1.18 | :white_check_mark: | ## Why another library From fd416ce413f88ed69e0b2047e0ceba64dc9a4efe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Apr 2022 08:00:40 +0200 Subject: [PATCH 096/502] chore(deps): bump codecov/codecov-action from 2.1.0 to 3.0.0 (#171) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 2.1.0 to 3.0.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v2.1.0...v3.0.0) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c163975..af7c282 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov ./pkg/... - - uses: codecov/codecov-action@v2.1.0 + - uses: codecov/codecov-action@v3.0.0 with: file: ./profile.cov name: codecov-go From 478795ad79d65ac6336fba09f518cb250d82e89b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Apr 2022 08:00:56 +0200 Subject: [PATCH 097/502] chore(deps): bump actions/setup-go from 2 to 3 (#170) Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2 to 3. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/setup-go dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index af7c282..3865d29 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Setup go - uses: actions/setup-go@v2 + uses: actions/setup-go@v3 with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov ./pkg/... From c195452bb03eb42a7358a5a59aec86dbe0aafd39 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Thu, 14 Apr 2022 10:10:56 +0200 Subject: [PATCH 098/502] feat(rp): provide key by data (not only path) for jwt profile (#168) --- example/client/app/app.go | 2 +- pkg/client/rp/relaying_party.go | 42 ++++++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/example/client/app/app.go b/example/client/app/app.go index 6db1597..e0369e3 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -40,7 +40,7 @@ func main() { options = append(options, rp.WithPKCE(cookieHandler)) } if keyPath != "" { - options = append(options, rp.WithClientKey(keyPath)) + options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath))) } provider, err := rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...) diff --git a/pkg/client/rp/relaying_party.go b/pkg/client/rp/relaying_party.go index 98ab354..91622f3 100644 --- a/pkg/client/rp/relaying_party.go +++ b/pkg/client/rp/relaying_party.go @@ -233,14 +233,50 @@ func WithVerifierOpts(opts ...VerifierOption) Option { } } +// WithClientKey specifies the path to the key.json to be used for the JWT Profile Client Authentication on the token endpoint +// +//deprecated: use WithJWTProfile(SignerFromKeyPath(path)) instead func WithClientKey(path string) Option { + return WithJWTProfile(SignerFromKeyPath(path)) +} + +// WithJWTProfile creates a signer used for the JWT Profile Client Authentication on the token endpoint +func WithJWTProfile(signerFromKey SignerFromKey) Option { return func(rp *relyingParty) error { - config, err := client.ConfigFromKeyFile(path) + signer, err := signerFromKey() if err != nil { return err } - rp.signer, err = client.NewSignerFromPrivateKeyByte([]byte(config.Key), config.KeyID) - return err + rp.signer = signer + return nil + } +} + +type SignerFromKey func() (jose.Signer, error) + +func SignerFromKeyPath(path string) SignerFromKey { + return func() (jose.Signer, error) { + config, err := client.ConfigFromKeyFile(path) + if err != nil { + return nil, err + } + return client.NewSignerFromPrivateKeyByte([]byte(config.Key), config.KeyID) + } +} + +func SignerFromKeyFile(fileData []byte) SignerFromKey { + return func() (jose.Signer, error) { + config, err := client.ConfigFromKeyFileData(fileData) + if err != nil { + return nil, err + } + return client.NewSignerFromPrivateKeyByte([]byte(config.Key), config.KeyID) + } +} + +func SignerFromKeyAndKeyID(key []byte, keyID string) SignerFromKey { + return func() (jose.Signer, error) { + return client.NewSignerFromPrivateKeyByte(key, keyID) } } From 885fe0d45c3c0946425aba71a386611f37ca9527 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Thu, 21 Apr 2022 17:54:00 +0200 Subject: [PATCH 099/502] docs(example): implement OpenID Provider (#165) * chore(example): implement OpenID Provider * jwt profile and fixes * some comments * remove old op example * fix code flow example * add service user and update readme * fix password for example use * ignore example and mock folders for code coverage * Update example/server/internal/storage.go Co-authored-by: Silvan * Update client.go Co-authored-by: Silvan --- .codecov/codecov.yml | 5 +- README.md | 40 ++- example/doc.go | 10 + example/internal/mock/storage.go | 344 ------------------ example/server/default/default.go | 72 ---- example/server/internal/client.go | 189 ++++++++++ example/server/internal/oidc.go | 203 +++++++++++ example/server/internal/storage.go | 553 +++++++++++++++++++++++++++++ example/server/internal/token.go | 25 ++ example/server/internal/user.go | 24 ++ example/server/login.go | 115 ++++++ example/server/op.go | 126 +++++++ example/server/service-key1.json | 1 + 13 files changed, 1280 insertions(+), 427 deletions(-) delete mode 100644 example/internal/mock/storage.go delete mode 100644 example/server/default/default.go create mode 100644 example/server/internal/client.go create mode 100644 example/server/internal/oidc.go create mode 100644 example/server/internal/storage.go create mode 100644 example/server/internal/token.go create mode 100644 example/server/internal/user.go create mode 100644 example/server/login.go create mode 100644 example/server/op.go create mode 100644 example/server/service-key1.json diff --git a/.codecov/codecov.yml b/.codecov/codecov.yml index 8104953..b4394d2 100644 --- a/.codecov/codecov.yml +++ b/.codecov/codecov.yml @@ -19,4 +19,7 @@ parsers: comment: layout: "header, diff" behavior: default - require_changes: no \ No newline at end of file + require_changes: no +ignore: + - "example" + - "**/mock" diff --git a/README.md b/README.md index e841dad..b5cd075 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,31 @@ ## What Is It -This project is a easy to use client (RP) and server (OP) implementation for the `OIDC` (Open ID Connect) standard written for `Go`. +This project is an easy-to-use client (RP) and server (OP) implementation for the `OIDC` (OpenID Connect) standard written for `Go`. The RP is certified for the [basic](https://www.certification.openid.net/plan-detail.html?public=true&plan=uoprP0OO8Z4Qo) and [config](https://www.certification.openid.net/plan-detail.html?public=true&plan=AYSdLbzmWbu9X) profile. Whenever possible we tried to reuse / extend existing packages like `OAuth2 for Go`. +## Basic Overview + +The most important packages of the library: +
+/pkg
+    /client     clients using the OP for retrieving, exchanging and verifying tokens       
+        /rp     definition and implementation of an OIDC Relying Party (client)
+        /rs     definition and implementation of an OAuth Resource Server (API)
+    /op         definition and implementation of an OIDC OpenID Provider (server)
+    /oidc       definitions shared by clients and server
+
+/example
+    /api        example of an api / resource server implementation using token introspection
+    /app        web app / RP demonstrating authorization code flow using various authentication methods (code, PKCE, JWT profile)
+    /github     example of the extended OAuth2 library, providing an HTTP client with a reuse token source
+    /service    demonstration of JWT Profile Authorization Grant
+    /server     example of an OpenID Provider implementation including some very basic login UI
+
+ ## How To Use It Check the `/example` folder where example code for different scenarios is located. @@ -24,21 +43,22 @@ Check the `/example` folder where example code for different scenarios is locate ```bash # start oidc op server # oidc discovery http://localhost:9998/.well-known/openid-configuration -CAOS_OIDC_DEV=1 go run github.com/caos/oidc/example/server/default +go run github.com/caos/oidc/example/server # start oidc web client -CLIENT_ID=web CLIENT_SECRET=web ISSUER=http://localhost:9998/ SCOPES=openid PORT=5556 go run github.com/caos/oidc/example/client/app +CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998/ SCOPES="openid profile" PORT=9999 go run github.com/caos/oidc/example/client/app ``` -- browser http://localhost:5556/login will redirect to op server -- input id to login -- redirect to client app display user info +- open http://localhost:9999/login in your browser +- you will be redirected to op server and the login UI +- login with user `test-user` and password `verysecure` +- the OP will redirect you to the client app, which displays the user info ## Features -| | Code Flow | Implicit Flow | Hybrid Flow | Discovery | PKCE | Token Exchange | mTLS | JWT Profile | Refresh Token | -|----------------|-----------|---------------|-------------|-----------|------|----------------|---------|-------------|---------------| -| Relying Party | yes | no[^1] | no | yes | yes | partial | not yet | yes | yes | -| OpenID Provider | yes | yes | not yet | yes | yes | not yet | not yet | yes | yes | +| | Code Flow | Implicit Flow | Hybrid Flow | Discovery | PKCE | Token Exchange | mTLS | JWT Profile | Refresh Token | +|------------------|-----------|---------------|-------------|-----------|------|----------------|---------|-------------|---------------| +| Relying Party | yes | no[^1] | no | yes | yes | partial | not yet | yes | yes | +| OpenID Provider | yes | yes | not yet | yes | yes | not yet | not yet | yes | yes | ### Resources diff --git a/example/doc.go b/example/doc.go index f7ec372..7212a7d 100644 --- a/example/doc.go +++ b/example/doc.go @@ -1 +1,11 @@ +/* +Package example contains some example of the various use of this library: + +/api example of an api / resource server implementation using token introspection +/app web app / RP demonstrating authorization code flow using various authentication methods (code, PKCE, JWT profile) +/github example of the extended OAuth2 library, providing an HTTP client with a reuse token source +/service demonstration of JWT Profile Authorization Grant +/server example of an OpenID Provider implementation including some very basic login UI + +*/ package example diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go deleted file mode 100644 index 570e8a5..0000000 --- a/example/internal/mock/storage.go +++ /dev/null @@ -1,344 +0,0 @@ -package mock - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "errors" - "time" - - "gopkg.in/square/go-jose.v2" - - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/op" -) - -type AuthStorage struct { - key *rsa.PrivateKey -} - -func NewAuthStorage() op.Storage { - reader := rand.Reader - bitSize := 2048 - key, err := rsa.GenerateKey(reader, bitSize) - if err != nil { - panic(err) - } - return &AuthStorage{ - key: key, - } -} - -type AuthRequest struct { - ID string - ResponseType oidc.ResponseType - ResponseMode oidc.ResponseMode - RedirectURI string - Nonce string - ClientID string - CodeChallenge *oidc.CodeChallenge - State string -} - -func (a *AuthRequest) GetACR() string { - return "" -} - -func (a *AuthRequest) GetAMR() []string { - return []string{ - "password", - } -} - -func (a *AuthRequest) GetAudience() []string { - return []string{ - a.ClientID, - } -} - -func (a *AuthRequest) GetAuthTime() time.Time { - return time.Now().UTC() -} - -func (a *AuthRequest) GetClientID() string { - return a.ClientID -} - -func (a *AuthRequest) GetCode() string { - return "code" -} - -func (a *AuthRequest) GetCodeChallenge() *oidc.CodeChallenge { - return a.CodeChallenge -} - -func (a *AuthRequest) GetID() string { - return a.ID -} - -func (a *AuthRequest) GetNonce() string { - return a.Nonce -} - -func (a *AuthRequest) GetRedirectURI() string { - return a.RedirectURI - // return "http://localhost:5556/auth/callback" -} - -func (a *AuthRequest) GetResponseType() oidc.ResponseType { - return a.ResponseType -} - -func (a *AuthRequest) GetResponseMode() oidc.ResponseMode { - return a.ResponseMode -} - -func (a *AuthRequest) GetScopes() []string { - return []string{ - "openid", - "profile", - "email", - } -} - -func (a *AuthRequest) SetCurrentScopes(scopes []string) {} - -func (a *AuthRequest) GetState() string { - return a.State -} - -func (a *AuthRequest) GetSubject() string { - return "sub" -} - -func (a *AuthRequest) Done() bool { - return true -} - -var ( - a = &AuthRequest{} - t bool - c string -) - -func (s *AuthStorage) Health(ctx context.Context) error { - return nil -} - -func (s *AuthStorage) CreateAuthRequest(_ context.Context, authReq *oidc.AuthRequest, _ string) (op.AuthRequest, error) { - a = &AuthRequest{ID: "id", ClientID: authReq.ClientID, ResponseType: authReq.ResponseType, Nonce: authReq.Nonce, RedirectURI: authReq.RedirectURI, State: authReq.State} - if authReq.CodeChallenge != "" { - a.CodeChallenge = &oidc.CodeChallenge{ - Challenge: authReq.CodeChallenge, - Method: authReq.CodeChallengeMethod, - } - } - t = false - return a, nil -} -func (s *AuthStorage) AuthRequestByCode(_ context.Context, code string) (op.AuthRequest, error) { - if code != c { - return nil, errors.New("invalid code") - } - return a, nil -} -func (s *AuthStorage) SaveAuthCode(_ context.Context, id, code string) error { - if a.ID != id { - return errors.New("not found") - } - c = code - return nil -} -func (s *AuthStorage) DeleteAuthRequest(context.Context, string) error { - t = true - return nil -} -func (s *AuthStorage) AuthRequestByID(_ context.Context, id string) (op.AuthRequest, error) { - if id != "id" || t { - return nil, errors.New("not found") - } - return a, nil -} -func (s *AuthStorage) CreateAccessToken(ctx context.Context, request op.TokenRequest) (string, time.Time, error) { - return "id", time.Now().UTC().Add(5 * time.Minute), nil -} -func (s *AuthStorage) CreateAccessAndRefreshTokens(ctx context.Context, request op.TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) { - return "id", "refreshToken", time.Now().UTC().Add(5 * time.Minute), nil -} -func (s *AuthStorage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (op.RefreshTokenRequest, error) { - if refreshToken != c { - return nil, errors.New("invalid token") - } - return a, nil -} - -func (s *AuthStorage) TerminateSession(_ context.Context, userID, clientID string) error { - return nil -} - -func (s *AuthStorage) RevokeToken(ctx context.Context, token string, userID string, clientID string) *oidc.Error { - return nil -} - -func (s *AuthStorage) GetSigningKey(_ context.Context, keyCh chan<- jose.SigningKey) { - keyCh <- jose.SigningKey{Algorithm: jose.RS256, Key: s.key} -} -func (s *AuthStorage) GetKey(_ context.Context) (*rsa.PrivateKey, error) { - return s.key, nil -} -func (s *AuthStorage) GetKeySet(_ context.Context) (*jose.JSONWebKeySet, error) { - pubkey := s.key.Public() - return &jose.JSONWebKeySet{ - Keys: []jose.JSONWebKey{ - {Key: pubkey, Use: "sig", Algorithm: "RS256", KeyID: "1"}, - }, - }, nil -} -func (s *AuthStorage) GetKeyByIDAndUserID(_ context.Context, _, _ string) (*jose.JSONWebKey, error) { - pubkey := s.key.Public() - return &jose.JSONWebKey{Key: pubkey, Use: "sig", Algorithm: "RS256", KeyID: "1"}, nil -} - -func (s *AuthStorage) GetClientByClientID(_ context.Context, id string) (op.Client, error) { - if id == "none" { - return nil, errors.New("not found") - } - var appType op.ApplicationType - var authMethod oidc.AuthMethod - var accessTokenType op.AccessTokenType - var responseTypes []oidc.ResponseType - if id == "web" { - appType = op.ApplicationTypeWeb - authMethod = oidc.AuthMethodBasic - accessTokenType = op.AccessTokenTypeBearer - responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode} - } else if id == "native" { - appType = op.ApplicationTypeNative - authMethod = oidc.AuthMethodNone - accessTokenType = op.AccessTokenTypeBearer - responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode} - } else { - appType = op.ApplicationTypeUserAgent - authMethod = oidc.AuthMethodNone - accessTokenType = op.AccessTokenTypeJWT - responseTypes = []oidc.ResponseType{oidc.ResponseTypeIDToken, oidc.ResponseTypeIDTokenOnly} - } - return &ConfClient{ID: id, applicationType: appType, authMethod: authMethod, accessTokenType: accessTokenType, responseTypes: responseTypes, devMode: false, grantTypes: []oidc.GrantType{oidc.GrantTypeCode}}, nil -} - -func (s *AuthStorage) AuthorizeClientIDSecret(_ context.Context, id string, _ string) error { - return nil -} - -func (s *AuthStorage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, _, _, _ string) error { - return s.SetUserinfoFromScopes(ctx, userinfo, "", "", []string{}) -} -func (s *AuthStorage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, _, _ string, _ []string) error { - userinfo.SetSubject(a.GetSubject()) - userinfo.SetAddress(oidc.NewUserInfoAddress("Test 789\nPostfach 2", "", "", "", "", "")) - userinfo.SetEmail("test", true) - userinfo.SetPhone("0791234567", true) - userinfo.SetName("Test") - userinfo.AppendClaims("private_claim", "test") - return nil -} -func (s *AuthStorage) GetPrivateClaimsFromScopes(_ context.Context, _, _ string, _ []string) (map[string]interface{}, error) { - return map[string]interface{}{"private_claim": "test"}, nil -} - -func (s *AuthStorage) SetIntrospectionFromToken(ctx context.Context, introspect oidc.IntrospectionResponse, tokenID, subject, clientID string) error { - if err := s.SetUserinfoFromScopes(ctx, introspect, "", "", []string{}); err != nil { - return err - } - introspect.SetClientID(a.ClientID) - return nil -} - -func (s *AuthStorage) ValidateJWTProfileScopes(ctx context.Context, userID string, scope []string) ([]string, error) { - return scope, nil -} - -type ConfClient struct { - applicationType op.ApplicationType - authMethod oidc.AuthMethod - responseTypes []oidc.ResponseType - grantTypes []oidc.GrantType - ID string - accessTokenType op.AccessTokenType - devMode bool -} - -func (c *ConfClient) GetID() string { - return c.ID -} -func (c *ConfClient) RedirectURIs() []string { - return []string{ - "https://registered.com/callback", - "http://localhost:9999/callback", - "http://localhost:5556/auth/callback", - "custom://callback", - "https://localhost:8443/test/a/instructions-example/callback", - "https://op.certification.openid.net:62064/authz_cb", - "https://op.certification.openid.net:62064/authz_post", - } -} -func (c *ConfClient) PostLogoutRedirectURIs() []string { - return []string{} -} - -func (c *ConfClient) LoginURL(id string) string { - return "login?id=" + id -} - -func (c *ConfClient) ApplicationType() op.ApplicationType { - return c.applicationType -} - -func (c *ConfClient) AuthMethod() oidc.AuthMethod { - return c.authMethod -} - -func (c *ConfClient) IDTokenLifetime() time.Duration { - return 5 * time.Minute -} -func (c *ConfClient) AccessTokenType() op.AccessTokenType { - return c.accessTokenType -} -func (c *ConfClient) ResponseTypes() []oidc.ResponseType { - return c.responseTypes -} -func (c *ConfClient) GrantTypes() []oidc.GrantType { - return c.grantTypes -} - -func (c *ConfClient) DevMode() bool { - return c.devMode -} - -func (c *ConfClient) AllowedScopes() []string { - return nil -} - -func (c *ConfClient) RestrictAdditionalIdTokenScopes() func(scopes []string) []string { - return func(scopes []string) []string { - return scopes - } -} - -func (c *ConfClient) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { - return func(scopes []string) []string { - return scopes - } -} - -func (c *ConfClient) IsScopeAllowed(scope string) bool { - return false -} - -func (c *ConfClient) IDTokenUserinfoClaimsAssertion() bool { - return false -} - -func (c *ConfClient) ClockSkew() time.Duration { - return 0 -} diff --git a/example/server/default/default.go b/example/server/default/default.go deleted file mode 100644 index 7edaf2e..0000000 --- a/example/server/default/default.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "context" - "crypto/sha256" - "html/template" - "log" - "net/http" - - "github.com/gorilla/mux" - - "github.com/caos/oidc/example/internal/mock" - "github.com/caos/oidc/pkg/op" -) - -func main() { - ctx := context.Background() - port := "9998" - config := &op.Config{ - Issuer: "http://localhost:9998/", - CryptoKey: sha256.Sum256([]byte("test")), - } - storage := mock.NewAuthStorage() - handler, err := op.NewOpenIDProvider(ctx, config, storage, op.WithCustomTokenEndpoint(op.NewEndpoint("test"))) - if err != nil { - log.Fatal(err) - } - router := handler.HttpHandler().(*mux.Router) - router.Methods("GET").Path("/login").HandlerFunc(HandleLogin) - router.Methods("POST").Path("/login").HandlerFunc(HandleCallback) - server := &http.Server{ - Addr: ":" + port, - Handler: router, - } - err = server.ListenAndServe() - if err != nil { - log.Fatal(err) - } - <-ctx.Done() -} - -func HandleLogin(w http.ResponseWriter, r *http.Request) { - tpl := ` - - - - - Login - - -
- - -
- - ` - t, err := template.New("login").Parse(tpl) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - err = t.Execute(w, nil) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -func HandleCallback(w http.ResponseWriter, r *http.Request) { - r.ParseForm() - client := r.FormValue("client") - http.Redirect(w, r, "/authorize/callback?id="+client, http.StatusFound) -} diff --git a/example/server/internal/client.go b/example/server/internal/client.go new file mode 100644 index 0000000..55425a3 --- /dev/null +++ b/example/server/internal/client.go @@ -0,0 +1,189 @@ +package internal + +import ( + "time" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/op" +) + +var ( + //we use the default login UI and pass the (auth request) id + defaultLoginURL = func(id string) string { + return "/login/username?authRequestID=" + id + } + + //clients to be used by the storage interface + clients = map[string]*Client{} +) + +//Client represents the internal model of an OAuth/OIDC client +//this could also be your database model +type Client struct { + id string + secret string + redirectURIs []string + applicationType op.ApplicationType + authMethod oidc.AuthMethod + loginURL func(string) string + responseTypes []oidc.ResponseType + grantTypes []oidc.GrantType + accessTokenType op.AccessTokenType + devMode bool + idTokenUserinfoClaimsAssertion bool + clockSkew time.Duration +} + +//GetID must return the client_id +func (c *Client) GetID() string { + return c.id +} + +//RedirectURIs must return the registered redirect_uris for Code and Implicit Flow +func (c *Client) RedirectURIs() []string { + return c.redirectURIs +} + +//PostLogoutRedirectURIs must return the registered post_logout_redirect_uris for sign-outs +func (c *Client) PostLogoutRedirectURIs() []string { + return []string{} +} + +//ApplicationType must return the type of the client (app, native, user agent) +func (c *Client) ApplicationType() op.ApplicationType { + return c.applicationType +} + +//AuthMethod must return the authentication method (client_secret_basic, client_secret_post, none, private_key_jwt) +func (c *Client) AuthMethod() oidc.AuthMethod { + return c.authMethod +} + +//ResponseTypes must return all allowed response types (code, id_token token, id_token) +//these must match with the allowed grant types +func (c *Client) ResponseTypes() []oidc.ResponseType { + return c.responseTypes +} + +//GrantTypes must return all allowed grant types (authorization_code, refresh_token, urn:ietf:params:oauth:grant-type:jwt-bearer) +func (c *Client) GrantTypes() []oidc.GrantType { + return c.grantTypes +} + +//LoginURL will be called to redirect the user (agent) to the login UI +//you could implement some logic here to redirect the users to different login UIs depending on the client +func (c *Client) LoginURL(id string) string { + return c.loginURL(id) +} + +//AccessTokenType must return the type of access token the client uses (Bearer (opaque) or JWT) +func (c *Client) AccessTokenType() op.AccessTokenType { + return c.accessTokenType +} + +//IDTokenLifetime must return the lifetime of the client's id_tokens +func (c *Client) IDTokenLifetime() time.Duration { + return 1 * time.Hour +} + +//DevMode enables the use of non-compliant configs such as redirect_uris (e.g. http schema for user agent client) +func (c *Client) DevMode() bool { + return c.devMode +} + +//RestrictAdditionalIdTokenScopes allows specifying which custom scopes shall be asserted into the id_token +func (c *Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []string { + return func(scopes []string) []string { + return scopes + } +} + +//RestrictAdditionalAccessTokenScopes allows specifying which custom scopes shall be asserted into the JWT access_token +func (c *Client) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { + return func(scopes []string) []string { + return scopes + } +} + +//IsScopeAllowed enables Client specific custom scopes validation +//in this example we allow the CustomScope for all clients +func (c *Client) IsScopeAllowed(scope string) bool { + return scope == CustomScope +} + +//IDTokenUserinfoClaimsAssertion allows specifying if claims of scope profile, email, phone and address are asserted into the id_token +//even if an access token if issued which violates the OIDC Core spec +//(5.4. Requesting Claims using Scope Values: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) +//some clients though require that e.g. email is always in the id_token when requested even if an access_token is issued +func (c *Client) IDTokenUserinfoClaimsAssertion() bool { + return c.idTokenUserinfoClaimsAssertion +} + +//ClockSkew enables clients to instruct the OP to apply a clock skew on the various times and expirations +//(subtract from issued_at, add to expiration, ...) +func (c *Client) ClockSkew() time.Duration { + return c.clockSkew +} + +//RegisterClients enables you to register clients for the example implementation +//there are some clients (web and native) to try out different cases +//add more if necessary +func RegisterClients(registerClients ...*Client) { + for _, client := range registerClients { + clients[client.id] = client + } +} + +//NativeClient will create a client of type native, which will always use PKCE and allow the use of refresh tokens +//user-defined redirectURIs may include: +// - http://localhost without port specification (e.g. http://localhost/auth/callback) +// - custom protocol (e.g. custom://auth/callback) +//(the examples will be used as default, if none is provided) +func NativeClient(id string, redirectURIs ...string) *Client { + if len(redirectURIs) == 0 { + redirectURIs = []string{ + "http://localhost/auth/callback", + "custom://auth/callback", + } + } + return &Client{ + id: id, + secret: "", //no secret needed (due to PKCE) + redirectURIs: redirectURIs, + applicationType: op.ApplicationTypeNative, + authMethod: oidc.AuthMethodNone, + loginURL: defaultLoginURL, + responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode}, + grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken}, + accessTokenType: 0, + devMode: false, + idTokenUserinfoClaimsAssertion: false, + clockSkew: 0, + } +} + +//WebClient will create a client of type web, which will always use Basic Auth and allow the use of refresh tokens +//user-defined redirectURIs may include: +// - http://localhost with port specification (e.g. http://localhost:9999/auth/callback) +//(the example will be used as default, if none is provided) +func WebClient(id, secret string, redirectURIs ...string) *Client { + if len(redirectURIs) == 0 { + redirectURIs = []string{ + "http://localhost:9999/auth/callback", + } + } + return &Client{ + id: id, + secret: secret, + redirectURIs: redirectURIs, + applicationType: op.ApplicationTypeWeb, + authMethod: oidc.AuthMethodBasic, + loginURL: defaultLoginURL, + responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode}, + grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken}, + accessTokenType: 0, + devMode: false, + idTokenUserinfoClaimsAssertion: false, + clockSkew: 0, + } +} diff --git a/example/server/internal/oidc.go b/example/server/internal/oidc.go new file mode 100644 index 0000000..1b3bf52 --- /dev/null +++ b/example/server/internal/oidc.go @@ -0,0 +1,203 @@ +package internal + +import ( + "time" + + "golang.org/x/text/language" + + "github.com/caos/oidc/pkg/op" + + "github.com/caos/oidc/pkg/oidc" +) + +const ( + //CustomScope is an example for how to use custom scopes in this library + //(in this scenario, when requested, it will return a custom claim) + CustomScope = "custom_scope" + + //CustomClaim is an example for how to return custom claims with this library + CustomClaim = "custom_claim" +) + +type AuthRequest struct { + ID string + CreationDate time.Time + ApplicationID string + CallbackURI string + TransferState string + Prompt []string + UiLocales []language.Tag + LoginHint string + MaxAuthAge *time.Duration + UserID string + Scopes []string + ResponseType oidc.ResponseType + Nonce string + CodeChallenge *OIDCCodeChallenge + + passwordChecked bool + authTime time.Time +} + +func (a *AuthRequest) GetID() string { + return a.ID +} + +func (a *AuthRequest) GetACR() string { + return "" //we won't handle acr in this example +} + +func (a *AuthRequest) GetAMR() []string { + //this example only uses password for authentication + if a.passwordChecked { + return []string{"pwd"} + } + return nil +} + +func (a *AuthRequest) GetAudience() []string { + return []string{a.ApplicationID} //this example will always just use the client_id as audience +} + +func (a *AuthRequest) GetAuthTime() time.Time { + return a.authTime +} + +func (a *AuthRequest) GetClientID() string { + return a.ApplicationID +} + +func (a *AuthRequest) GetCodeChallenge() *oidc.CodeChallenge { + return CodeChallengeToOIDC(a.CodeChallenge) +} + +func (a *AuthRequest) GetNonce() string { + return a.Nonce +} + +func (a *AuthRequest) GetRedirectURI() string { + return a.CallbackURI +} + +func (a *AuthRequest) GetResponseType() oidc.ResponseType { + return a.ResponseType +} + +func (a *AuthRequest) GetResponseMode() oidc.ResponseMode { + return "" //we won't handle response mode in this example +} + +func (a *AuthRequest) GetScopes() []string { + return a.Scopes +} + +func (a *AuthRequest) GetState() string { + return a.TransferState +} + +func (a *AuthRequest) GetSubject() string { + return a.UserID +} + +func (a *AuthRequest) Done() bool { + return a.passwordChecked //this example only uses password for authentication +} + +func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string { + prompts := make([]string, len(oidcPrompt)) + for _, oidcPrompt := range oidcPrompt { + switch oidcPrompt { + case oidc.PromptNone, + oidc.PromptLogin, + oidc.PromptConsent, + oidc.PromptSelectAccount: + prompts = append(prompts, oidcPrompt) + } + } + return prompts +} + +func MaxAgeToInternal(maxAge *uint) *time.Duration { + if maxAge == nil { + return nil + } + dur := time.Duration(*maxAge) * time.Second + return &dur +} + +func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthRequest { + return &AuthRequest{ + CreationDate: time.Now(), + ApplicationID: authReq.ClientID, + CallbackURI: authReq.RedirectURI, + TransferState: authReq.State, + Prompt: PromptToInternal(authReq.Prompt), + UiLocales: authReq.UILocales, + LoginHint: authReq.LoginHint, + MaxAuthAge: MaxAgeToInternal(authReq.MaxAge), + UserID: userID, + Scopes: authReq.Scopes, + ResponseType: authReq.ResponseType, + Nonce: authReq.Nonce, + CodeChallenge: &OIDCCodeChallenge{ + Challenge: authReq.CodeChallenge, + Method: string(authReq.CodeChallengeMethod), + }, + } +} + +type OIDCCodeChallenge struct { + Challenge string + Method string +} + +func CodeChallengeToOIDC(challenge *OIDCCodeChallenge) *oidc.CodeChallenge { + if challenge == nil { + return nil + } + challengeMethod := oidc.CodeChallengeMethodPlain + if challenge.Method == "S256" { + challengeMethod = oidc.CodeChallengeMethodS256 + } + return &oidc.CodeChallenge{ + Challenge: challenge.Challenge, + Method: challengeMethod, + } +} + +//RefreshTokenRequestFromBusiness will simply wrap the internal RefreshToken to implement the op.RefreshTokenRequest interface +func RefreshTokenRequestFromBusiness(token *RefreshToken) op.RefreshTokenRequest { + return &RefreshTokenRequest{token} +} + +type RefreshTokenRequest struct { + *RefreshToken +} + +func (r *RefreshTokenRequest) GetAMR() []string { + return r.AMR +} + +func (r *RefreshTokenRequest) GetAudience() []string { + return r.Audience +} + +func (r *RefreshTokenRequest) GetAuthTime() time.Time { + return r.AuthTime +} + +func (r *RefreshTokenRequest) GetClientID() string { + return r.ApplicationID +} + +func (r *RefreshTokenRequest) GetScopes() []string { + return r.Scopes +} + +func (r *RefreshTokenRequest) GetSubject() string { + return r.UserID +} + +func (r *RefreshTokenRequest) SetCurrentScopes(scopes []string) { + r.Scopes = scopes +} diff --git a/example/server/internal/storage.go b/example/server/internal/storage.go new file mode 100644 index 0000000..8d61050 --- /dev/null +++ b/example/server/internal/storage.go @@ -0,0 +1,553 @@ +package internal + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "fmt" + "math/big" + "time" + + "github.com/google/uuid" + "golang.org/x/text/language" + "gopkg.in/square/go-jose.v2" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/op" +) + +var ( + //serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant + //the corresponding private key is in the service-key1.json (for demonstration purposes) + serviceKey1 = &rsa.PublicKey{ + N: func() *big.Int { + n, _ := new(big.Int).SetString("00f6d44fb5f34ac2033a75e73cb65ff24e6181edc58845e75a560ac21378284977bb055b1a75b714874e2a2641806205681c09abec76efd52cf40984edcf4c8ca09717355d11ac338f280d3e4c905b00543bdb8ee5a417496cb50cb0e29afc5a0d0471fd5a2fa625bd5281f61e6b02067d4fe7a5349eeae6d6a4300bcd86eef331", 16) + return n + }(), + E: 65537, + } +) + +//storage implements the op.Storage interface +//typically you would implement this as a layer on top of your database +//for simplicity this example keeps everything in-memory +type storage struct { + authRequests map[string]*AuthRequest + codes map[string]string + tokens map[string]*Token + clients map[string]*Client + users map[string]*User + services map[string]Service + refreshTokens map[string]*RefreshToken + signingKey signingKey +} + +type signingKey struct { + ID string + Algorithm string + Key *rsa.PrivateKey +} + +func NewStorage() *storage { + key, _ := rsa.GenerateKey(rand.Reader, 2048) + return &storage{ + authRequests: make(map[string]*AuthRequest), + codes: make(map[string]string), + tokens: make(map[string]*Token), + refreshTokens: make(map[string]*RefreshToken), + clients: clients, + users: map[string]*User{ + "id1": { + id: "id1", + username: "test-user", + password: "verysecure", + firstname: "Test", + lastname: "User", + email: "test-user@zitadel.ch", + emailVerified: true, + phone: "", + phoneVerified: false, + preferredLanguage: language.German, + }, + }, + services: map[string]Service{ + "service": { + keys: map[string]*rsa.PublicKey{ + "key1": serviceKey1, + }, + }, + }, + signingKey: signingKey{ + ID: "id", + Algorithm: "RS256", + Key: key, + }, + } +} + +//CheckUsernamePassword implements the `authenticate` interface of the login +func (s *storage) CheckUsernamePassword(username, password, id string) error { + request, ok := s.authRequests[id] + if !ok { + return fmt.Errorf("request not found") + } + + //for demonstration purposes we'll check on a static list with plain text password + //for real world scenarios, be sure to have the password hashed and salted (e.g. using bcrypt) + for _, user := range s.users { + if user.username == username && user.password == password { + //be sure to set user id into the auth request after the user was checked, + //so that you'll be able to get more information about the user after the login + request.UserID = user.id + + //you will have to change some state on the request to guide the user through possible multiple steps of the login process + //in this example we'll simply check the username / password and set a boolean to true + //therefore we will also just check this boolean if the request / login has been finished + request.passwordChecked = true + return nil + } + } + return fmt.Errorf("username or password wrong") +} + +//CreateAuthRequest implements the op.Storage interface +//it will be called after parsing and validation of the authentication request +func (s *storage) CreateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, userID string) (op.AuthRequest, error) { + //typically, you'll fill your internal / storage model with the information of the passed object + request := authRequestToInternal(authReq, userID) + + //you'll also have to create a unique id for the request (this might be done by your database; we'll use a uuid) + request.ID = uuid.NewString() + + //and save it in your database (for demonstration purposed we will use a simple map) + s.authRequests[request.ID] = request + + //finally, return the request (which implements the AuthRequest interface of the OP + return request, nil +} + +//AuthRequestByID implements the op.Storage interface +//it will be called after the Login UI redirects back to the OIDC endpoint +func (s *storage) AuthRequestByID(ctx context.Context, id string) (op.AuthRequest, error) { + request, ok := s.authRequests[id] + if !ok { + return nil, fmt.Errorf("request not found") + } + return request, nil +} + +//AuthRequestByCode implements the op.Storage interface +//it will be called after parsing and validation of the token request (in an authorization code flow) +func (s *storage) AuthRequestByCode(ctx context.Context, code string) (op.AuthRequest, error) { + //for this example we read the id by code and then get the request by id + requestID, ok := s.codes[code] + if !ok { + return nil, fmt.Errorf("code invalid or expired") + } + return s.AuthRequestByID(ctx, requestID) +} + +//SaveAuthCode implements the op.Storage interface +//it will be called after the authentication has been successful and before redirecting the user agent to the redirect_uri +//(in an authorization code flow) +func (s *storage) SaveAuthCode(ctx context.Context, id string, code string) error { + //for this example we'll just save the authRequestID to the code + s.codes[code] = id + return nil +} + +//DeleteAuthRequest implements the op.Storage interface +//it will be called after creating the token response (id and access tokens) for a valid +//- authentication request (in an implicit flow) +//- token request (in an authorization code flow) +func (s *storage) DeleteAuthRequest(ctx context.Context, id string) error { + //you can simply delete all reference to the auth request + delete(s.authRequests, id) + for code, requestID := range s.codes { + if id == requestID { + delete(s.codes, code) + return nil + } + } + return nil +} + +//CreateAccessToken implements the op.Storage interface +//it will be called for all requests able to return an access token (Authorization Code Flow, Implicit Flow, JWT Profile, ...) +func (s *storage) CreateAccessToken(ctx context.Context, request op.TokenRequest) (string, time.Time, error) { + var applicationID string + //if authenticated for an app (auth code / implicit flow) we must save the client_id to the token + authReq, ok := request.(*AuthRequest) + if ok { + applicationID = authReq.ApplicationID + } + token, err := s.accessToken(applicationID, "", request.GetSubject(), request.GetAudience(), request.GetScopes()) + if err != nil { + return "", time.Time{}, err + } + return token.ID, token.Expiration, nil +} + +//CreateAccessAndRefreshTokens implements the op.Storage interface +//it will be called for all requests able to return an access and refresh token (Authorization Code Flow, Refresh Token Request) +func (s *storage) CreateAccessAndRefreshTokens(ctx context.Context, request op.TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) { + //get the information depending on the request type / implementation + applicationID, authTime, amr := getInfoFromRequest(request) + + //if currentRefreshToken is empty (Code Flow) we will have to create a new refresh token + if currentRefreshToken == "" { + refreshTokenID := uuid.NewString() + accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes()) + if err != nil { + return "", "", time.Time{}, err + } + refreshToken, err := s.createRefreshToken(accessToken, amr, authTime) + if err != nil { + return "", "", time.Time{}, err + } + return accessToken.ID, refreshToken, accessToken.Expiration, nil + } + + //if we get here, the currentRefreshToken was not empty, so the call is a refresh token request + //we therefore will have to check the currentRefreshToken and renew the refresh token + refreshToken, refreshTokenID, err := s.renewRefreshToken(currentRefreshToken) + if err != nil { + return "", "", time.Time{}, err + } + accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes()) + if err != nil { + return "", "", time.Time{}, err + } + return accessToken.ID, refreshToken, accessToken.Expiration, nil +} + +//TokenRequestByRefreshToken implements the op.Storage interface +//it will be called after parsing and validation of the refresh token request +func (s *storage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (op.RefreshTokenRequest, error) { + token, ok := s.refreshTokens[refreshToken] + if !ok { + return nil, fmt.Errorf("invalid refresh_token") + } + return RefreshTokenRequestFromBusiness(token), nil +} + +//TerminateSession implements the op.Storage interface +//it will be called after the user signed out, therefore the access and refresh token of the user of this client must be removed +func (s *storage) TerminateSession(ctx context.Context, userID string, clientID string) error { + for _, token := range s.tokens { + if token.ApplicationID == clientID && token.Subject == userID { + delete(s.tokens, token.ID) + delete(s.refreshTokens, token.RefreshTokenID) + return nil + } + } + return nil +} + +//RevokeToken implements the op.Storage interface +//it will be called after parsing and validation of the token revocation request +func (s *storage) RevokeToken(ctx context.Context, token string, userID string, clientID string) *oidc.Error { + //a single token was requested to be removed + accessToken, ok := s.tokens[token] + if ok { + if accessToken.ApplicationID != clientID { + return oidc.ErrInvalidClient().WithDescription("token was not issued for this client") + } + //if it is an access token, just remove it + //you could also remove the corresponding refresh token if really necessary + delete(s.tokens, accessToken.ID) + return nil + } + refreshToken, ok := s.refreshTokens[token] + if !ok { + //if the token is neither an access nor a refresh token, just ignore it, the expected behaviour of + //being not valid (anymore) is achieved + return nil + } + if refreshToken.ApplicationID != clientID { + return oidc.ErrInvalidClient().WithDescription("token was not issued for this client") + } + //if it is a refresh token, you will have to remove the access token as well + delete(s.refreshTokens, refreshToken.ID) + for _, accessToken := range s.tokens { + if accessToken.RefreshTokenID == refreshToken.ID { + delete(s.tokens, accessToken.ID) + return nil + } + } + return nil +} + +//GetSigningKey implements the op.Storage interface +//it will be called when creating the OpenID Provider +func (s *storage) GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey) { + //in this example the signing key is a static rsa.PrivateKey and the algorithm used is RS256 + //you would obviously have a more complex implementation and store / retrieve the key from your database as well + // + //the idea of the signing key channel is, that you can (with what ever mechanism) rotate your signing key and + //switch the key of the signer via this channel + keyCh <- jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(s.signingKey.Algorithm), //always tell the signer with algorithm to use + Key: jose.JSONWebKey{ + KeyID: s.signingKey.ID, //always give the key an id so, that it will include it in the token header as `kid` claim + Key: s.signingKey.Key, + }, + } +} + +//GetKeySet implements the op.Storage interface +//it will be called to get the current (public) keys, among others for the keys_endpoint or for validating access_tokens on the userinfo_endpoint, ... +func (s *storage) GetKeySet(ctx context.Context) (*jose.JSONWebKeySet, error) { + //as mentioned above, this example only has a single signing key without key rotation, + //so it will directly use its public key + // + //when using key rotation you typically would store the public keys alongside the private keys in your database + //and give both of them an expiration date, with the public key having a longer lifetime (e.g. rotate private key every + return &jose.JSONWebKeySet{Keys: []jose.JSONWebKey{ + { + KeyID: s.signingKey.ID, + Algorithm: s.signingKey.Algorithm, + Use: oidc.KeyUseSignature, + Key: &s.signingKey.Key.PublicKey, + }}, + }, nil +} + +//GetClientByClientID implements the op.Storage interface +//it will be called whenever information (type, redirect_uris, ...) about the client behind the client_id is needed +func (s *storage) GetClientByClientID(ctx context.Context, clientID string) (op.Client, error) { + client, ok := s.clients[clientID] + if !ok { + return nil, fmt.Errorf("client not found") + } + return client, nil +} + +//AuthorizeClientIDSecret implements the op.Storage interface +//it will be called for validating the client_id, client_secret on token or introspection requests +func (s *storage) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error { + client, ok := s.clients[clientID] + if !ok { + return fmt.Errorf("client not found") + } + //for this example we directly check the secret + //obviously you would not have the secret in plain text, but rather hashed and salted (e.g. using bcrypt) + if client.secret != clientSecret { + return fmt.Errorf("invalid secret") + } + return nil +} + +//SetUserinfoFromScopes implements the op.Storage interface +//it will be called for the creation of an id_token, so we'll just pass it to the private function without any further check +func (s *storage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error { + return s.setUserinfo(ctx, userinfo, userID, clientID, scopes) +} + +//SetUserinfoFromToken implements the op.Storage interface +//it will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function +func (s *storage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error { + token, ok := s.tokens[tokenID] + if !ok { + return fmt.Errorf("token is invalid or has expired") + } + //the userinfo endpoint should support CORS. If it's not possible to specify a specific origin in the CORS handler, + //and you have to specify a wildcard (*) origin, then you could also check here if the origin which called the userinfo endpoint here directly + //note that the origin can be empty (if called by a web client) + // + //if origin != "" { + // client, ok := s.clients[token.ApplicationID] + // if !ok { + // return fmt.Errorf("client not found") + // } + // if err := checkAllowedOrigins(client.allowedOrigins, origin); err != nil { + // return err + // } + //} + return s.setUserinfo(ctx, userinfo, token.Subject, token.ApplicationID, token.Scopes) +} + +//SetIntrospectionFromToken implements the op.Storage interface +//it will be called for the introspection endpoint, so we read the token and pass the information from that to the private function +func (s *storage) SetIntrospectionFromToken(ctx context.Context, introspection oidc.IntrospectionResponse, tokenID, subject, clientID string) error { + token, ok := s.tokens[tokenID] + if !ok { + return fmt.Errorf("token is invalid or has expired") + } + //check if the client is part of the requested audience + for _, aud := range token.Audience { + if aud == clientID { + //the introspection response only has to return a boolean (active) if the token is active + //this will automatically be done by the library if you don't return an error + //you can also return further information about the user / associated token + //e.g. the userinfo (equivalent to userinfo endpoint) + err := s.setUserinfo(ctx, introspection, subject, clientID, token.Scopes) + if err != nil { + return err + } + //...and also the requested scopes... + introspection.SetScopes(token.Scopes) + //...and the client the token was issued to + introspection.SetClientID(token.ApplicationID) + return nil + } + } + return fmt.Errorf("token is not valid for this client") +} + +//GetPrivateClaimsFromScopes implements the op.Storage interface +//it will be called for the creation of a JWT access token to assert claims for custom scopes +func (s *storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) { + for _, scope := range scopes { + switch scope { + case CustomScope: + claims = appendClaim(claims, CustomClaim, customClaim(clientID)) + } + } + return claims, nil +} + +//GetKeyByIDAndUserID implements the op.Storage interface +//it will be called to validate the signatures of a JWT (JWT Profile Grant and Authentication) +func (s *storage) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) { + service, ok := s.services[userID] + if !ok { + return nil, fmt.Errorf("user not found") + } + key, ok := service.keys[keyID] + if !ok { + return nil, fmt.Errorf("key not found") + } + return &jose.JSONWebKey{ + KeyID: keyID, + Use: "sig", + Key: key, + }, nil +} + +//ValidateJWTProfileScopes implements the op.Storage interface +//it will be called to validate the scopes of a JWT Profile Authorization Grant request +func (s *storage) ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error) { + allowedScopes := make([]string, 0) + for _, scope := range scopes { + if scope == oidc.ScopeOpenID { + allowedScopes = append(allowedScopes, scope) + } + } + return allowedScopes, nil +} + +//Health implements the op.Storage interface +func (s *storage) Health(ctx context.Context) error { + return nil +} + +//createRefreshToken will store a refresh_token in-memory based on the provided information +func (s *storage) createRefreshToken(accessToken *Token, amr []string, authTime time.Time) (string, error) { + token := &RefreshToken{ + ID: accessToken.RefreshTokenID, + Token: accessToken.RefreshTokenID, + AuthTime: authTime, + AMR: amr, + ApplicationID: accessToken.ApplicationID, + UserID: accessToken.Subject, + Audience: accessToken.Audience, + Expiration: time.Now().Add(5 * time.Hour), + Scopes: accessToken.Scopes, + } + s.refreshTokens[token.ID] = token + return token.Token, nil +} + +//renewRefreshToken checks the provided refresh_token and creates a new one based on the current +func (s *storage) renewRefreshToken(currentRefreshToken string) (string, string, error) { + refreshToken, ok := s.refreshTokens[currentRefreshToken] + if !ok { + return "", "", fmt.Errorf("invalid refresh token") + } + //deletes the refresh token and all access tokens which were issued based on this refresh token + delete(s.refreshTokens, currentRefreshToken) + for _, token := range s.tokens { + if token.RefreshTokenID == currentRefreshToken { + delete(s.tokens, token.ID) + break + } + } + //creates a new refresh token based on the current one + token := uuid.NewString() + refreshToken.Token = token + s.refreshTokens[token] = refreshToken + return token, refreshToken.ID, nil +} + +//accessToken will store an access_token in-memory based on the provided information +func (s *storage) accessToken(applicationID, refreshTokenID, subject string, audience, scopes []string) (*Token, error) { + token := &Token{ + ID: uuid.NewString(), + ApplicationID: applicationID, + RefreshTokenID: refreshTokenID, + Subject: subject, + Audience: audience, + Expiration: time.Now().Add(5 * time.Minute), + Scopes: scopes, + } + s.tokens[token.ID] = token + return token, nil +} + +//setUserinfo sets the info based on the user, scopes and if necessary the clientID +func (s *storage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSetter, userID, clientID string, scopes []string) (err error) { + user, ok := s.users[userID] + if !ok { + return fmt.Errorf("user not found") + } + for _, scope := range scopes { + switch scope { + case oidc.ScopeOpenID: + userInfo.SetSubject(user.id) + case oidc.ScopeEmail: + userInfo.SetEmail(user.email, user.emailVerified) + case oidc.ScopeProfile: + userInfo.SetPreferredUsername(user.username) + userInfo.SetName(user.firstname + " " + user.lastname) + userInfo.SetFamilyName(user.lastname) + userInfo.SetGivenName(user.firstname) + userInfo.SetLocale(user.preferredLanguage) + case oidc.ScopePhone: + userInfo.SetPhone(user.phone, user.phoneVerified) + case CustomScope: + //you can also have a custom scope and assert public or custom claims based on that + userInfo.AppendClaims(CustomClaim, customClaim(clientID)) + } + } + return nil +} + +//getInfoFromRequest returns the clientID, authTime and amr depending on the op.TokenRequest type / implementation +func getInfoFromRequest(req op.TokenRequest) (clientID string, authTime time.Time, amr []string) { + authReq, ok := req.(*AuthRequest) //Code Flow (with scope offline_access) + if ok { + return authReq.ApplicationID, authReq.authTime, authReq.GetAMR() + } + refreshReq, ok := req.(*RefreshTokenRequest) //Refresh Token Request + if ok { + return refreshReq.ApplicationID, refreshReq.AuthTime, refreshReq.AMR + } + return "", time.Time{}, nil +} + +//customClaim demonstrates how to return custom claims based on provided information +func customClaim(clientID string) map[string]interface{} { + return map[string]interface{}{ + "client": clientID, + "other": "stuff", + } +} + +func appendClaim(claims map[string]interface{}, claim string, value interface{}) map[string]interface{} { + if claims == nil { + claims = make(map[string]interface{}) + } + claims[claim] = value + return claims +} diff --git a/example/server/internal/token.go b/example/server/internal/token.go new file mode 100644 index 0000000..09e675a --- /dev/null +++ b/example/server/internal/token.go @@ -0,0 +1,25 @@ +package internal + +import "time" + +type Token struct { + ID string + ApplicationID string + Subject string + RefreshTokenID string + Audience []string + Expiration time.Time + Scopes []string +} + +type RefreshToken struct { + ID string + Token string + AuthTime time.Time + AMR []string + Audience []string + UserID string + ApplicationID string + Expiration time.Time + Scopes []string +} diff --git a/example/server/internal/user.go b/example/server/internal/user.go new file mode 100644 index 0000000..19b5d1f --- /dev/null +++ b/example/server/internal/user.go @@ -0,0 +1,24 @@ +package internal + +import ( + "crypto/rsa" + + "golang.org/x/text/language" +) + +type User struct { + id string + username string + password string + firstname string + lastname string + email string + emailVerified bool + phone string + phoneVerified bool + preferredLanguage language.Tag +} + +type Service struct { + keys map[string]*rsa.PublicKey +} diff --git a/example/server/login.go b/example/server/login.go new file mode 100644 index 0000000..90d01d8 --- /dev/null +++ b/example/server/login.go @@ -0,0 +1,115 @@ +package main + +import ( + "fmt" + "html/template" + "net/http" + + "github.com/gorilla/mux" +) + +const ( + queryAuthRequestID = "authRequestID" +) + +var ( + loginTmpl, _ = template.New("login").Parse(` + + + + + Login + + +
+ + + +
+ + +
+ +
+ + +
+ +

{{.Error}}

+ + +
+ + `) +) + +type login struct { + authenticate authenticate + router *mux.Router + callback func(string) string +} + +func NewLogin(authenticate authenticate, callback func(string) string) *login { + l := &login{ + authenticate: authenticate, + callback: callback, + } + l.createRouter() + return l +} + +func (l *login) createRouter() { + l.router = mux.NewRouter() + l.router.Path("/username").Methods("GET").HandlerFunc(l.loginHandler) + l.router.Path("/username").Methods("POST").HandlerFunc(l.checkLoginHandler) +} + +type authenticate interface { + CheckUsernamePassword(username, password, id string) error +} + +func (l *login) loginHandler(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + http.Error(w, fmt.Sprintf("cannot parse form:%s", err), http.StatusInternalServerError) + return + } + //the oidc package will pass the id of the auth request as query parameter + //we will use this id through the login process and therefore pass it to the login page + renderLogin(w, r.FormValue(queryAuthRequestID), nil) +} + +func renderLogin(w http.ResponseWriter, id string, err error) { + var errMsg string + if err != nil { + errMsg = err.Error() + } + data := &struct { + ID string + Error string + }{ + ID: id, + Error: errMsg, + } + err = loginTmpl.Execute(w, data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (l *login) checkLoginHandler(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + http.Error(w, fmt.Sprintf("cannot parse form:%s", err), http.StatusInternalServerError) + return + } + username := r.FormValue("username") + password := r.FormValue("password") + id := r.FormValue("id") + err = l.authenticate.CheckUsernamePassword(username, password, id) + if err != nil { + renderLogin(w, id, err) + return + } + http.Redirect(w, r, l.callback(id), http.StatusFound) +} diff --git a/example/server/op.go b/example/server/op.go new file mode 100644 index 0000000..54b5041 --- /dev/null +++ b/example/server/op.go @@ -0,0 +1,126 @@ +package main + +import ( + "context" + "crypto/sha256" + "fmt" + "log" + "net/http" + "os" + + "github.com/gorilla/mux" + "golang.org/x/text/language" + + "github.com/caos/oidc/example/server/internal" + "github.com/caos/oidc/pkg/op" +) + +const ( + pathLoggedOut = "/logged-out" +) + +func init() { + internal.RegisterClients( + internal.NativeClient("native"), + internal.WebClient("web", "secret"), + internal.WebClient("api", "secret"), + ) +} + +func main() { + ctx := context.Background() + + //this will allow us to use an issuer with http:// instead of https:// + os.Setenv(op.OidcDevMode, "true") + + port := "9998" + + //the OpenID Provider requires a 32-byte key for (token) encryption + //be sure to create a proper crypto random key and manage it securely! + key := sha256.Sum256([]byte("test")) + + router := mux.NewRouter() + + //for simplicity, we provide a very small default page for users who have signed out + router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) { + _, err := w.Write([]byte("signed out successfully")) + if err != nil { + log.Printf("error serving logged out page: %v", err) + } + }) + + //the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations + //this might be the layer for accessing your database + //in this example it will be handled in-memory + storage := internal.NewStorage() + + //creation of the OpenIDProvider with the just created in-memory Storage + provider, err := newOP(ctx, storage, port, key) + if err != nil { + log.Fatal(err) + } + + //the provider will only take care of the OpenID Protocol, so there must be some sort of UI for the login process + //for the simplicity of the example this means a simple page with username and password field + l := NewLogin(storage, op.AuthCallbackURL(provider)) + + //regardless of how many pages / steps there are in the process, the UI must be registered in the router, + //so we will direct all calls to /login to the login UI + router.PathPrefix("/login/").Handler(http.StripPrefix("/login", l.router)) + + //we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration) + //is served on the correct path + // + //if your issuer ends with a path (e.g. http://localhost:9998/custom/path/), + //then you would have to set the path prefix (/custom/path/) + router.PathPrefix("/").Handler(provider.HttpHandler()) + + server := &http.Server{ + Addr: ":" + port, + Handler: router, + } + err = server.ListenAndServe() + if err != nil { + log.Fatal(err) + } + <-ctx.Done() +} + +//newOP will create an OpenID Provider for localhost on a specified port with a given encryption key +//and a predefined default logout uri +//it will enable all options (see descriptions) +func newOP(ctx context.Context, storage op.Storage, port string, key [32]byte) (op.OpenIDProvider, error) { + config := &op.Config{ + Issuer: fmt.Sprintf("http://localhost:%s/", port), + CryptoKey: key, + + //will be used if the end_session endpoint is called without a post_logout_redirect_uri + DefaultLogoutRedirectURI: pathLoggedOut, + + //enables code_challenge_method S256 for PKCE (and therefore PKCE in general) + CodeMethodS256: true, + + //enables additional client_id/client_secret authentication by form post (not only HTTP Basic Auth) + AuthMethodPost: true, + + //enables additional authentication by using private_key_jwt + AuthMethodPrivateKeyJWT: true, + + //enables refresh_token grant use + GrantTypeRefreshToken: true, + + //enables use of the `request` Object parameter + RequestObjectSupported: true, + + //this example has only static texts (in English), so we'll set the here accordingly + SupportedUILocales: []language.Tag{language.English}, + } + handler, err := op.NewOpenIDProvider(ctx, config, storage, + //as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth + op.WithCustomAuthEndpoint(op.NewEndpoint("auth")), + ) + if err != nil { + return nil, err + } + return handler, nil +} diff --git a/example/server/service-key1.json b/example/server/service-key1.json new file mode 100644 index 0000000..a0d20e8 --- /dev/null +++ b/example/server/service-key1.json @@ -0,0 +1 @@ +{"type":"serviceaccount","keyId":"key1","key":"-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQD21E+180rCAzp15zy2X/JOYYHtxYhF51pWCsITeChJd7sFWxp1\ntxSHTiomQYBiBWgcCavsdu/VLPQJhO3PTIyglxc1XRGsM48oDT5MkFsAVDvbjuWk\nF0lstQyw4pr8Wg0Ecf1aL6YlvVKB9h5rAgZ9T+elNJ7q5takMAvNhu7zMQIDAQAB\nAoGAeLRw2qjEaUZM43WWchVPmFcEw/MyZgTyX1tZd03uXacolUDtGp3ScyydXiHw\nF39PX063fabYOCaInNMdvJ9RsQz2OcZuS/K6NOmWhzBfLgs4Y1tU6ijoY/gBjHgu\nCV0KjvoWIfEtKl/On/wTrAnUStFzrc7U4dpKFP1fy2ZTTnECQQD8aP2QOxmKUyfg\nBAjfonpkrNeaTRNwTULTvEHFiLyaeFd1PAvsDiKZtpk6iHLb99mQZkVVtAK5qgQ4\n1OI72jkVAkEA+lcAamuZAM+gIiUhbHA7BfX9OVgyGDD2tx5g/kxhMUmK6hIiO6Ul\n0nw5KfrCEUU3AzrM7HejUg3q61SYcXTgrQJBALhrzbhwNf0HPP9Ec2dSw7KDRxSK\ndEV9bfJefn/hpEwI2X3i3aMfwNAmxlYqFCH8OY5z6vzvhX46ZtNPV+z7SPECQQDq\nApXi5P27YlpgULEzup2R7uZsymLZdjvJ5V3pmOBpwENYlublNnVqkrCk60CqADdy\nj26rxRIoS9ZDcWqm9AhpAkEAyrNXBMJh08ghBMb3NYPFfr/bftRJSrGjhBPuJ5qr\nXzWaXhYVMMh3OSAwzHBJbA1ffdQJuH2ebL99Ur5fpBcbVw==\n-----END RSA PRIVATE KEY-----\n","userId":"service"} From c07c504f7f88c55dac9d6decc33b6c60afdeac28 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Apr 2022 21:40:15 +0200 Subject: [PATCH 100/502] chore(deps): bump codecov/codecov-action from 3.0.0 to 3.1.0 (#175) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3865d29..2496a7c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov ./pkg/... - - uses: codecov/codecov-action@v3.0.0 + - uses: codecov/codecov-action@v3.1.0 with: file: ./profile.cov name: codecov-go From 72f28a10ceee558b141228dbb68d670ebb1fd81d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Apr 2022 21:41:08 +0200 Subject: [PATCH 101/502] chore(deps): bump github/codeql-action from 1 to 2 (#176) --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6bdfcdf..7b852d2 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -29,7 +29,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 # Override language selection by uncommenting this and choosing your languages with: languages: go @@ -37,7 +37,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -51,4 +51,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 From 550f7877f2ac22993ebd923e5f64cfbb9b225ab2 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Tue, 26 Apr 2022 23:48:29 +0200 Subject: [PATCH 102/502] fix: move to new org (#177) * chore: move to new org * chore: change import * fix: update logging lib Co-authored-by: Fabienne Co-authored-by: adlerhurst --- CONTRIBUTING.md | 10 +++++----- README.md | 16 ++++++++-------- SECURITY.md | 2 +- example/client/api/api.go | 4 ++-- example/client/app/app.go | 6 +++--- example/client/github/github.go | 6 +++--- example/client/service/service.go | 2 +- example/server/internal/client.go | 4 ++-- example/server/internal/oidc.go | 4 ++-- example/server/internal/storage.go | 4 ++-- example/server/op.go | 4 ++-- go.mod | 4 ++-- go.sum | 4 ++-- pkg/client/client.go | 6 +++--- pkg/client/jwt_profile.go | 4 ++-- pkg/client/profile/jwt_profile.go | 4 ++-- pkg/client/rp/cli/cli.go | 6 +++--- pkg/client/rp/delegation.go | 2 +- pkg/client/rp/jwks.go | 4 ++-- pkg/client/rp/mock/generate.go | 2 +- pkg/client/rp/mock/verifier.mock.go | 4 ++-- pkg/client/rp/relaying_party.go | 6 +++--- pkg/client/rp/tockenexchange.go | 2 +- pkg/client/rp/verifier.go | 2 +- pkg/client/rs/resource_server.go | 6 +++--- pkg/oidc/code_challenge.go | 2 +- pkg/oidc/token.go | 4 ++-- pkg/oidc/verifier.go | 2 +- pkg/op/auth_request.go | 6 +++--- pkg/op/auth_request_test.go | 8 ++++---- pkg/op/client.go | 2 +- pkg/op/crypto.go | 2 +- pkg/op/discovery.go | 4 ++-- pkg/op/discovery_test.go | 6 +++--- pkg/op/endpoint_test.go | 2 +- pkg/op/error.go | 4 ++-- pkg/op/keys.go | 2 +- pkg/op/keys_test.go | 6 +++--- pkg/op/mock/authorizer.mock.go | 6 +++--- pkg/op/mock/authorizer.mock.impl.go | 4 ++-- pkg/op/mock/client.go | 4 ++-- pkg/op/mock/client.mock.go | 6 +++--- pkg/op/mock/configuration.mock.go | 4 ++-- pkg/op/mock/generate.go | 12 ++++++------ pkg/op/mock/key.mock.go | 2 +- pkg/op/mock/signer.mock.go | 2 +- pkg/op/mock/storage.mock.go | 6 +++--- pkg/op/mock/storage.mock.impl.go | 5 +++-- pkg/op/op.go | 4 ++-- pkg/op/probes.go | 2 +- pkg/op/session.go | 4 ++-- pkg/op/signer.go | 2 +- pkg/op/storage.go | 2 +- pkg/op/token.go | 6 +++--- pkg/op/token_code.go | 4 ++-- pkg/op/token_intospection.go | 4 ++-- pkg/op/token_jwt_profile.go | 4 ++-- pkg/op/token_refresh.go | 6 +++--- pkg/op/token_request.go | 4 ++-- pkg/op/token_revocation.go | 4 ++-- pkg/op/userinfo.go | 4 ++-- pkg/op/verifier_access_token.go | 2 +- pkg/op/verifier_id_token_hint.go | 2 +- pkg/op/verifier_jwt_profile.go | 2 +- 64 files changed, 138 insertions(+), 137 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f0c8ac7..8861b9c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,13 +2,13 @@ ## Did you find a bug? -Please file an issue [here](https://github.com/caos/oidc/issues/new?assignees=&labels=bug&template=bug_report.md&title=). +Please file an issue [here](https://github.com/zitadel/oidc/issues/new?assignees=&labels=bug&template=bug_report.md&title=). Bugs are evaluated every day as soon as possible. ## Enhancement -Do you miss a feature? Please file an issue [here](https://github.com/caos/oidc/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=) +Do you miss a feature? Please file an issue [here](https://github.com/zitadel/oidc/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=) Enhancements are discussed and evaluated every Wednesday by the ZITADEL core team. @@ -16,8 +16,8 @@ Enhancements are discussed and evaluated every Wednesday by the ZITADEL core tea We add the label "good first issue" for problems we think are a good starting point to contribute to the OIDC SDK. -* [Issues for first time contributors](https://github.com/caos/oidc/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) -* [All issues](https://github.com/caos/oidc/issues) +* [Issues for first time contributors](https://github.com/zitadel/oidc/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) +* [All issues](https://github.com/zitadel/oidc/issues) ### Make a PR @@ -33,7 +33,7 @@ Make sure you use semantic release: Checkout the [examples folder](example) for different client and server implementations. -Or checkout how we use it ourselves in our OpenSource Identity and Access Management [ZITADEL](https://github.com/caos/zitadel). +Or checkout how we use it ourselves in our OpenSource Identity and Access Management [ZITADEL](https://github.com/zitadel/zitadel). ## **Did you find a security flaw?** diff --git a/README.md b/README.md index b5cd075..d71ff13 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # OpenID Connect SDK (client and server) for Go [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) -[![Release](https://github.com/caos/oidc/workflows/Release/badge.svg)](https://github.com/caos/oidc/actions) -[![license](https://badgen.net/github/license/caos/oidc/)](https://github.com/caos/oidc/blob/master/LICENSE) -[![release](https://badgen.net/github/release/caos/oidc/stable)](https://github.com/caos/oidc/releases) -[![Go Report Card](https://goreportcard.com/badge/github.com/caos/oidc)](https://goreportcard.com/report/github.com/caos/oidc) -[![codecov](https://codecov.io/gh/caos/oidc/branch/master/graph/badge.svg)](https://codecov.io/gh/caos/oidc) +[![Release](https://github.com/zitadel/oidc/workflows/Release/badge.svg)](https://github.com/zitadel/oidc/actions) +[![license](https://badgen.net/github/license/zitadel/oidc/)](https://github.com/zitadel/oidc/blob/master/LICENSE) +[![release](https://badgen.net/github/release/zitadel/oidc/stable)](https://github.com/zitadel/oidc/releases) +[![Go Report Card](https://goreportcard.com/badge/github.com/zitadel/oidc)](https://goreportcard.com/report/github.com/zitadel/oidc) +[![codecov](https://codecov.io/gh/zitadel/oidc/branch/master/graph/badge.svg)](https://codecov.io/gh/zitadel/oidc) ![openid_certified](https://cloud.githubusercontent.com/assets/1454075/7611268/4d19de32-f97b-11e4-895b-31b2455a7ca6.png) @@ -43,9 +43,9 @@ Check the `/example` folder where example code for different scenarios is locate ```bash # start oidc op server # oidc discovery http://localhost:9998/.well-known/openid-configuration -go run github.com/caos/oidc/example/server +go run github.com/zitadel/oidc/example/server # start oidc web client -CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998/ SCOPES="openid profile" PORT=9999 go run github.com/caos/oidc/example/client/app +CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/example/client/app ``` - open http://localhost:9999/login in your browser @@ -110,4 +110,4 @@ See the exact licensing terms [here](./LICENSE) Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -[^1]: https://github.com/caos/oidc/issues/135#issuecomment-950563892 +[^1]: https://github.com/zitadel/oidc/issues/135#issuecomment-950563892 diff --git a/SECURITY.md b/SECURITY.md index 6fe2daa..dca11f3 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -35,7 +35,7 @@ TBD ## Public Disclosure -All accepted and mitigated vulnerabilitys will be published on the [Github Security Page](https://github.com/caos/oidc/security/advisories) +All accepted and mitigated vulnerabilitys will be published on the [Github Security Page](https://github.com/zitadel/oidc/security/advisories) ### Timing diff --git a/example/client/api/api.go b/example/client/api/api.go index a3ae85e..4fe09ed 100644 --- a/example/client/api/api.go +++ b/example/client/api/api.go @@ -12,8 +12,8 @@ import ( "github.com/gorilla/mux" "github.com/sirupsen/logrus" - "github.com/caos/oidc/pkg/client/rs" - "github.com/caos/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/client/rs" + "github.com/zitadel/oidc/pkg/oidc" ) const ( diff --git a/example/client/app/app.go b/example/client/app/app.go index e0369e3..10453b1 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -11,9 +11,9 @@ import ( "github.com/google/uuid" "github.com/sirupsen/logrus" - "github.com/caos/oidc/pkg/client/rp" - httphelper "github.com/caos/oidc/pkg/http" - "github.com/caos/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/client/rp" + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" ) var ( diff --git a/example/client/github/github.go b/example/client/github/github.go index 45f16c1..feb3e26 100644 --- a/example/client/github/github.go +++ b/example/client/github/github.go @@ -10,9 +10,9 @@ import ( "golang.org/x/oauth2" githubOAuth "golang.org/x/oauth2/github" - "github.com/caos/oidc/pkg/client/rp" - "github.com/caos/oidc/pkg/client/rp/cli" - "github.com/caos/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/client/rp" + "github.com/zitadel/oidc/pkg/client/rp/cli" + "github.com/zitadel/oidc/pkg/http" ) var ( diff --git a/example/client/service/service.go b/example/client/service/service.go index 818b481..f406c6d 100644 --- a/example/client/service/service.go +++ b/example/client/service/service.go @@ -13,7 +13,7 @@ import ( "github.com/sirupsen/logrus" "golang.org/x/oauth2" - "github.com/caos/oidc/pkg/client/profile" + "github.com/zitadel/oidc/pkg/client/profile" ) var ( diff --git a/example/server/internal/client.go b/example/server/internal/client.go index 55425a3..9080e8c 100644 --- a/example/server/internal/client.go +++ b/example/server/internal/client.go @@ -3,8 +3,8 @@ package internal import ( "time" - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/op" + "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/op" ) var ( diff --git a/example/server/internal/oidc.go b/example/server/internal/oidc.go index 1b3bf52..5edf970 100644 --- a/example/server/internal/oidc.go +++ b/example/server/internal/oidc.go @@ -5,9 +5,9 @@ import ( "golang.org/x/text/language" - "github.com/caos/oidc/pkg/op" + "github.com/zitadel/oidc/pkg/op" - "github.com/caos/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/oidc" ) const ( diff --git a/example/server/internal/storage.go b/example/server/internal/storage.go index 8d61050..5fd61c5 100644 --- a/example/server/internal/storage.go +++ b/example/server/internal/storage.go @@ -12,8 +12,8 @@ import ( "golang.org/x/text/language" "gopkg.in/square/go-jose.v2" - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/op" + "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/op" ) var ( diff --git a/example/server/op.go b/example/server/op.go index 54b5041..d689247 100644 --- a/example/server/op.go +++ b/example/server/op.go @@ -11,8 +11,8 @@ import ( "github.com/gorilla/mux" "golang.org/x/text/language" - "github.com/caos/oidc/example/server/internal" - "github.com/caos/oidc/pkg/op" + "github.com/zitadel/oidc/example/server/internal" + "github.com/zitadel/oidc/pkg/op" ) const ( diff --git a/go.mod b/go.mod index f23a1b3..570a3ba 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,8 @@ -module github.com/caos/oidc +module github.com/zitadel/oidc go 1.15 require ( - github.com/caos/logging v0.3.1 github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.2 // indirect github.com/google/go-github/v31 v31.0.0 @@ -15,6 +14,7 @@ require ( github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.7.1 + github.com/zitadel/logging v0.3.3 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 golang.org/x/text v0.3.7 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect diff --git a/go.sum b/go.sum index 605a76e..92f343e 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,6 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/caos/logging v0.3.1 h1:892AMeHs09D0e3ZcGB+QDRsZ5+2xtPAsAhOy8eKfztc= -github.com/caos/logging v0.3.1/go.mod h1:B8QNS0WDmR2Keac52Fw+XN4ZJkzLDGrcRIPB2Ux4uRo= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -145,6 +143,8 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zitadel/logging v0.3.3 h1:/nAoki9HFJK+qMLBVY5Jhbfp/6o3YLK49Tw5j2oRhjM= +github.com/zitadel/logging v0.3.3/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0= 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= diff --git a/pkg/client/client.go b/pkg/client/client.go index ac6cd56..58986bb 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -10,9 +10,9 @@ import ( "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" - "github.com/caos/oidc/pkg/crypto" - httphelper "github.com/caos/oidc/pkg/http" - "github.com/caos/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/crypto" + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" ) var ( diff --git a/pkg/client/jwt_profile.go b/pkg/client/jwt_profile.go index e120541..8bb6f4b 100644 --- a/pkg/client/jwt_profile.go +++ b/pkg/client/jwt_profile.go @@ -5,8 +5,8 @@ import ( "golang.org/x/oauth2" - "github.com/caos/oidc/pkg/http" - "github.com/caos/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" ) //JWTProfileExchange handles the oauth2 jwt profile exchange diff --git a/pkg/client/profile/jwt_profile.go b/pkg/client/profile/jwt_profile.go index 6b7db2c..03fa52a 100644 --- a/pkg/client/profile/jwt_profile.go +++ b/pkg/client/profile/jwt_profile.go @@ -7,8 +7,8 @@ import ( "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" - "github.com/caos/oidc/pkg/client" - "github.com/caos/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/client" + "github.com/zitadel/oidc/pkg/oidc" ) //jwtProfileTokenSource implement the oauth2.TokenSource diff --git a/pkg/client/rp/cli/cli.go b/pkg/client/rp/cli/cli.go index aba1546..6e30e4e 100644 --- a/pkg/client/rp/cli/cli.go +++ b/pkg/client/rp/cli/cli.go @@ -4,9 +4,9 @@ import ( "context" "net/http" - "github.com/caos/oidc/pkg/client/rp" - httphelper "github.com/caos/oidc/pkg/http" - "github.com/caos/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/client/rp" + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" ) const ( diff --git a/pkg/client/rp/delegation.go b/pkg/client/rp/delegation.go index 73edd96..463a5c4 100644 --- a/pkg/client/rp/delegation.go +++ b/pkg/client/rp/delegation.go @@ -1,7 +1,7 @@ package rp import ( - "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" + "github.com/zitadel/oidc/pkg/oidc/grants/tokenexchange" ) //DelegationTokenRequest is an implementation of TokenExchangeRequest diff --git a/pkg/client/rp/jwks.go b/pkg/client/rp/jwks.go index 78f9580..0624d6b 100644 --- a/pkg/client/rp/jwks.go +++ b/pkg/client/rp/jwks.go @@ -9,8 +9,8 @@ import ( "gopkg.in/square/go-jose.v2" - httphelper "github.com/caos/oidc/pkg/http" - "github.com/caos/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" ) func NewRemoteKeySet(client *http.Client, jwksURL string, opts ...func(*remoteKeySet)) oidc.KeySet { diff --git a/pkg/client/rp/mock/generate.go b/pkg/client/rp/mock/generate.go index 71bc3be..1e05701 100644 --- a/pkg/client/rp/mock/generate.go +++ b/pkg/client/rp/mock/generate.go @@ -1,3 +1,3 @@ package mock -//go:generate mockgen -package mock -destination ./verifier.mock.go github.com/caos/oidc/pkg/rp Verifier +//go:generate mockgen -package mock -destination ./verifier.mock.go github.com/zitadel/oidc/pkg/rp Verifier diff --git a/pkg/client/rp/mock/verifier.mock.go b/pkg/client/rp/mock/verifier.mock.go index 08cf77f..b20db68 100644 --- a/pkg/client/rp/mock/verifier.mock.go +++ b/pkg/client/rp/mock/verifier.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/caos/oidc/pkg/rp (interfaces: Verifier) +// Source: github.com/zitadel/oidc/pkg/rp (interfaces: Verifier) // Package mock is a generated GoMock package. package mock @@ -10,7 +10,7 @@ import ( "github.com/golang/mock/gomock" - "github.com/caos/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/oidc" ) // MockVerifier is a mock of Verifier interface diff --git a/pkg/client/rp/relaying_party.go b/pkg/client/rp/relaying_party.go index 91622f3..610a903 100644 --- a/pkg/client/rp/relaying_party.go +++ b/pkg/client/rp/relaying_party.go @@ -12,9 +12,9 @@ import ( "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" - "github.com/caos/oidc/pkg/client" - httphelper "github.com/caos/oidc/pkg/http" - "github.com/caos/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/client" + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" ) const ( diff --git a/pkg/client/rp/tockenexchange.go b/pkg/client/rp/tockenexchange.go index d5056ae..a1cb3be 100644 --- a/pkg/client/rp/tockenexchange.go +++ b/pkg/client/rp/tockenexchange.go @@ -5,7 +5,7 @@ import ( "golang.org/x/oauth2" - "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" + "github.com/zitadel/oidc/pkg/oidc/grants/tokenexchange" ) //TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange` diff --git a/pkg/client/rp/verifier.go b/pkg/client/rp/verifier.go index 027ca79..6cf5614 100644 --- a/pkg/client/rp/verifier.go +++ b/pkg/client/rp/verifier.go @@ -6,7 +6,7 @@ import ( "gopkg.in/square/go-jose.v2" - "github.com/caos/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/oidc" ) type IDTokenVerifier interface { diff --git a/pkg/client/rs/resource_server.go b/pkg/client/rs/resource_server.go index 224442f..63b29d4 100644 --- a/pkg/client/rs/resource_server.go +++ b/pkg/client/rs/resource_server.go @@ -6,9 +6,9 @@ import ( "net/http" "time" - "github.com/caos/oidc/pkg/client" - httphelper "github.com/caos/oidc/pkg/http" - "github.com/caos/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/client" + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" ) type ResourceServer interface { diff --git a/pkg/oidc/code_challenge.go b/pkg/oidc/code_challenge.go index 4e82feb..e1e459c 100644 --- a/pkg/oidc/code_challenge.go +++ b/pkg/oidc/code_challenge.go @@ -3,7 +3,7 @@ package oidc import ( "crypto/sha256" - "github.com/caos/oidc/pkg/crypto" + "github.com/zitadel/oidc/pkg/crypto" ) const ( diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index e34543e..7b5b812 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -9,8 +9,8 @@ import ( "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" - "github.com/caos/oidc/pkg/crypto" - "github.com/caos/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/crypto" + "github.com/zitadel/oidc/pkg/http" ) const ( diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go index 9f5335d..474e52e 100644 --- a/pkg/oidc/verifier.go +++ b/pkg/oidc/verifier.go @@ -12,7 +12,7 @@ import ( "gopkg.in/square/go-jose.v2" - str "github.com/caos/oidc/pkg/strings" + str "github.com/zitadel/oidc/pkg/strings" ) type Claims interface { diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 909b8b0..8a2b8b9 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -10,9 +10,9 @@ import ( "github.com/gorilla/mux" - httphelper "github.com/caos/oidc/pkg/http" - "github.com/caos/oidc/pkg/oidc" - str "github.com/caos/oidc/pkg/strings" + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" + str "github.com/zitadel/oidc/pkg/strings" ) type AuthRequest interface { diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index 7259ec7..f9ba4de 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -13,10 +13,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - httphelper "github.com/caos/oidc/pkg/http" - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/op" - "github.com/caos/oidc/pkg/op/mock" + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/op" + "github.com/zitadel/oidc/pkg/op/mock" ) // diff --git a/pkg/op/client.go b/pkg/op/client.go index f1e18fa..d3d32b7 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -3,7 +3,7 @@ package op import ( "time" - "github.com/caos/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/oidc" ) const ( diff --git a/pkg/op/crypto.go b/pkg/op/crypto.go index e9dd67b..f14b1de 100644 --- a/pkg/op/crypto.go +++ b/pkg/op/crypto.go @@ -1,7 +1,7 @@ package op import ( - "github.com/caos/oidc/pkg/crypto" + "github.com/zitadel/oidc/pkg/crypto" ) type Crypto interface { diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 955d0fa..6aca120 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -3,8 +3,8 @@ package op import ( "net/http" - httphelper "github.com/caos/oidc/pkg/http" - "github.com/caos/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" ) func discoveryHandler(c Configuration, s Signer) func(http.ResponseWriter, *http.Request) { diff --git a/pkg/op/discovery_test.go b/pkg/op/discovery_test.go index 1f0663d..1d74f75 100644 --- a/pkg/op/discovery_test.go +++ b/pkg/op/discovery_test.go @@ -10,9 +10,9 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/square/go-jose.v2" - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/op" - "github.com/caos/oidc/pkg/op/mock" + "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/op" + "github.com/zitadel/oidc/pkg/op/mock" ) func TestDiscover(t *testing.T) { diff --git a/pkg/op/endpoint_test.go b/pkg/op/endpoint_test.go index fe00326..96472b2 100644 --- a/pkg/op/endpoint_test.go +++ b/pkg/op/endpoint_test.go @@ -3,7 +3,7 @@ package op_test import ( "testing" - "github.com/caos/oidc/pkg/op" + "github.com/zitadel/oidc/pkg/op" ) func TestEndpoint_Path(t *testing.T) { diff --git a/pkg/op/error.go b/pkg/op/error.go index ea8d368..3c820d6 100644 --- a/pkg/op/error.go +++ b/pkg/op/error.go @@ -3,8 +3,8 @@ package op import ( "net/http" - httphelper "github.com/caos/oidc/pkg/http" - "github.com/caos/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" ) type ErrAuthRequest interface { diff --git a/pkg/op/keys.go b/pkg/op/keys.go index e637066..a80211e 100644 --- a/pkg/op/keys.go +++ b/pkg/op/keys.go @@ -6,7 +6,7 @@ import ( "gopkg.in/square/go-jose.v2" - httphelper "github.com/caos/oidc/pkg/http" + httphelper "github.com/zitadel/oidc/pkg/http" ) type KeyProvider interface { diff --git a/pkg/op/keys_test.go b/pkg/op/keys_test.go index bf60a3e..7618589 100644 --- a/pkg/op/keys_test.go +++ b/pkg/op/keys_test.go @@ -11,9 +11,9 @@ import ( "github.com/stretchr/testify/assert" "gopkg.in/square/go-jose.v2" - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/op" - "github.com/caos/oidc/pkg/op/mock" + "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/op" + "github.com/zitadel/oidc/pkg/op/mock" ) func TestKeys(t *testing.T) { diff --git a/pkg/op/mock/authorizer.mock.go b/pkg/op/mock/authorizer.mock.go index 3c18022..898522f 100644 --- a/pkg/op/mock/authorizer.mock.go +++ b/pkg/op/mock/authorizer.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/caos/oidc/pkg/op (interfaces: Authorizer) +// Source: github.com/zitadel/oidc/pkg/op (interfaces: Authorizer) // Package mock is a generated GoMock package. package mock @@ -7,8 +7,8 @@ package mock import ( reflect "reflect" - http "github.com/caos/oidc/pkg/http" - op "github.com/caos/oidc/pkg/op" + http "github.com/zitadel/oidc/pkg/http" + op "github.com/zitadel/oidc/pkg/op" gomock "github.com/golang/mock/gomock" ) diff --git a/pkg/op/mock/authorizer.mock.impl.go b/pkg/op/mock/authorizer.mock.impl.go index a481a8b..6df6115 100644 --- a/pkg/op/mock/authorizer.mock.impl.go +++ b/pkg/op/mock/authorizer.mock.impl.go @@ -8,8 +8,8 @@ import ( "github.com/gorilla/schema" "gopkg.in/square/go-jose.v2" - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/op" + "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/op" ) func NewAuthorizer(t *testing.T) op.Authorizer { diff --git a/pkg/op/mock/client.go b/pkg/op/mock/client.go index b7ac3e8..7d559ae 100644 --- a/pkg/op/mock/client.go +++ b/pkg/op/mock/client.go @@ -5,8 +5,8 @@ import ( "github.com/golang/mock/gomock" - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/op" + "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/op" ) func NewClient(t *testing.T) op.Client { diff --git a/pkg/op/mock/client.mock.go b/pkg/op/mock/client.mock.go index f78754d..46eb674 100644 --- a/pkg/op/mock/client.mock.go +++ b/pkg/op/mock/client.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/caos/oidc/pkg/op (interfaces: Client) +// Source: github.com/zitadel/oidc/pkg/op (interfaces: Client) // Package mock is a generated GoMock package. package mock @@ -8,8 +8,8 @@ import ( reflect "reflect" time "time" - oidc "github.com/caos/oidc/pkg/oidc" - op "github.com/caos/oidc/pkg/op" + oidc "github.com/zitadel/oidc/pkg/oidc" + op "github.com/zitadel/oidc/pkg/op" gomock "github.com/golang/mock/gomock" ) diff --git a/pkg/op/mock/configuration.mock.go b/pkg/op/mock/configuration.mock.go index 3eb4542..55c13af 100644 --- a/pkg/op/mock/configuration.mock.go +++ b/pkg/op/mock/configuration.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/caos/oidc/pkg/op (interfaces: Configuration) +// Source: github.com/zitadel/oidc/pkg/op (interfaces: Configuration) // Package mock is a generated GoMock package. package mock @@ -7,7 +7,7 @@ package mock import ( reflect "reflect" - op "github.com/caos/oidc/pkg/op" + op "github.com/zitadel/oidc/pkg/op" gomock "github.com/golang/mock/gomock" language "golang.org/x/text/language" ) diff --git a/pkg/op/mock/generate.go b/pkg/op/mock/generate.go index 4dd020e..c9c7efa 100644 --- a/pkg/op/mock/generate.go +++ b/pkg/op/mock/generate.go @@ -1,8 +1,8 @@ package mock -//go:generate mockgen -package mock -destination ./storage.mock.go github.com/caos/oidc/pkg/op Storage -//go:generate mockgen -package mock -destination ./authorizer.mock.go github.com/caos/oidc/pkg/op Authorizer -//go:generate mockgen -package mock -destination ./client.mock.go github.com/caos/oidc/pkg/op Client -//go:generate mockgen -package mock -destination ./configuration.mock.go github.com/caos/oidc/pkg/op Configuration -//go:generate mockgen -package mock -destination ./signer.mock.go github.com/caos/oidc/pkg/op Signer -//go:generate mockgen -package mock -destination ./key.mock.go github.com/caos/oidc/pkg/op KeyProvider +//go:generate mockgen -package mock -destination ./storage.mock.go github.com/zitadel/oidc/pkg/op Storage +//go:generate mockgen -package mock -destination ./authorizer.mock.go github.com/zitadel/oidc/pkg/op Authorizer +//go:generate mockgen -package mock -destination ./client.mock.go github.com/zitadel/oidc/pkg/op Client +//go:generate mockgen -package mock -destination ./configuration.mock.go github.com/zitadel/oidc/pkg/op Configuration +//go:generate mockgen -package mock -destination ./signer.mock.go github.com/zitadel/oidc/pkg/op Signer +//go:generate mockgen -package mock -destination ./key.mock.go github.com/zitadel/oidc/pkg/op KeyProvider diff --git a/pkg/op/mock/key.mock.go b/pkg/op/mock/key.mock.go index 37e0677..56d12dc 100644 --- a/pkg/op/mock/key.mock.go +++ b/pkg/op/mock/key.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/caos/oidc/pkg/op (interfaces: KeyProvider) +// Source: github.com/zitadel/oidc/pkg/op (interfaces: KeyProvider) // Package mock is a generated GoMock package. package mock diff --git a/pkg/op/mock/signer.mock.go b/pkg/op/mock/signer.mock.go index 0564aa1..42a92fb 100644 --- a/pkg/op/mock/signer.mock.go +++ b/pkg/op/mock/signer.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/caos/oidc/pkg/op (interfaces: Signer) +// Source: github.com/zitadel/oidc/pkg/op (interfaces: Signer) // Package mock is a generated GoMock package. package mock diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index 0763230..e8743d4 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/caos/oidc/pkg/op (interfaces: Storage) +// Source: github.com/zitadel/oidc/pkg/op (interfaces: Storage) // Package mock is a generated GoMock package. package mock @@ -9,8 +9,8 @@ import ( reflect "reflect" time "time" - oidc "github.com/caos/oidc/pkg/oidc" - op "github.com/caos/oidc/pkg/op" + oidc "github.com/zitadel/oidc/pkg/oidc" + op "github.com/zitadel/oidc/pkg/op" gomock "github.com/golang/mock/gomock" jose "gopkg.in/square/go-jose.v2" ) diff --git a/pkg/op/mock/storage.mock.impl.go b/pkg/op/mock/storage.mock.impl.go index 4855cf5..5b74a50 100644 --- a/pkg/op/mock/storage.mock.impl.go +++ b/pkg/op/mock/storage.mock.impl.go @@ -3,15 +3,16 @@ package mock import ( "context" "errors" - "github.com/caos/oidc/pkg/oidc" "testing" "time" + "github.com/zitadel/oidc/pkg/oidc" + "gopkg.in/square/go-jose.v2" "github.com/golang/mock/gomock" - "github.com/caos/oidc/pkg/op" + "github.com/zitadel/oidc/pkg/op" ) func NewStorage(t *testing.T) op.Storage { diff --git a/pkg/op/op.go b/pkg/op/op.go index e910bf6..99afca3 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -12,8 +12,8 @@ import ( "golang.org/x/text/language" "gopkg.in/square/go-jose.v2" - httphelper "github.com/caos/oidc/pkg/http" - "github.com/caos/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" ) const ( diff --git a/pkg/op/probes.go b/pkg/op/probes.go index b6fdde2..f328951 100644 --- a/pkg/op/probes.go +++ b/pkg/op/probes.go @@ -5,7 +5,7 @@ import ( "errors" "net/http" - httphelper "github.com/caos/oidc/pkg/http" + httphelper "github.com/zitadel/oidc/pkg/http" ) type ProbesFn func(context.Context) error diff --git a/pkg/op/session.go b/pkg/op/session.go index 1f9290e..a1c2d90 100644 --- a/pkg/op/session.go +++ b/pkg/op/session.go @@ -4,8 +4,8 @@ import ( "context" "net/http" - httphelper "github.com/caos/oidc/pkg/http" - "github.com/caos/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" ) type SessionEnder interface { diff --git a/pkg/op/signer.go b/pkg/op/signer.go index 0dabf67..d05bbe5 100644 --- a/pkg/op/signer.go +++ b/pkg/op/signer.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/caos/logging" + "github.com/zitadel/logging" "gopkg.in/square/go-jose.v2" ) diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 94c2a33..710a7dd 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -6,7 +6,7 @@ import ( "gopkg.in/square/go-jose.v2" - "github.com/caos/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/oidc" ) type AuthStorage interface { diff --git a/pkg/op/token.go b/pkg/op/token.go index 3e97360..7f6f599 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -4,9 +4,9 @@ import ( "context" "time" - "github.com/caos/oidc/pkg/crypto" - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/strings" + "github.com/zitadel/oidc/pkg/crypto" + "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/strings" ) type TokenCreator interface { diff --git a/pkg/op/token_code.go b/pkg/op/token_code.go index 7b5873c..b21871e 100644 --- a/pkg/op/token_code.go +++ b/pkg/op/token_code.go @@ -4,8 +4,8 @@ import ( "context" "net/http" - httphelper "github.com/caos/oidc/pkg/http" - "github.com/caos/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" ) //CodeExchange handles the OAuth 2.0 authorization_code grant, including diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go index 8fd9187..f402c8b 100644 --- a/pkg/op/token_intospection.go +++ b/pkg/op/token_intospection.go @@ -5,8 +5,8 @@ import ( "net/http" "net/url" - httphelper "github.com/caos/oidc/pkg/http" - "github.com/caos/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" ) type Introspector interface { diff --git a/pkg/op/token_jwt_profile.go b/pkg/op/token_jwt_profile.go index 01a1411..0fccfe7 100644 --- a/pkg/op/token_jwt_profile.go +++ b/pkg/op/token_jwt_profile.go @@ -5,8 +5,8 @@ import ( "net/http" "time" - httphelper "github.com/caos/oidc/pkg/http" - "github.com/caos/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" ) type JWTAuthorizationGrantExchanger interface { diff --git a/pkg/op/token_refresh.go b/pkg/op/token_refresh.go index 0b6d470..5558a1d 100644 --- a/pkg/op/token_refresh.go +++ b/pkg/op/token_refresh.go @@ -6,9 +6,9 @@ import ( "net/http" "time" - httphelper "github.com/caos/oidc/pkg/http" - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/strings" + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/strings" ) type RefreshTokenRequest interface { diff --git a/pkg/op/token_request.go b/pkg/op/token_request.go index 6732bb1..712edca 100644 --- a/pkg/op/token_request.go +++ b/pkg/op/token_request.go @@ -5,8 +5,8 @@ import ( "net/http" "net/url" - httphelper "github.com/caos/oidc/pkg/http" - "github.com/caos/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" ) type Exchanger interface { diff --git a/pkg/op/token_revocation.go b/pkg/op/token_revocation.go index fbaf8b7..b4ae266 100644 --- a/pkg/op/token_revocation.go +++ b/pkg/op/token_revocation.go @@ -6,8 +6,8 @@ import ( "net/url" "strings" - httphelper "github.com/caos/oidc/pkg/http" - "github.com/caos/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" ) type Revoker interface { diff --git a/pkg/op/userinfo.go b/pkg/op/userinfo.go index f07a8bc..4bd03e2 100644 --- a/pkg/op/userinfo.go +++ b/pkg/op/userinfo.go @@ -6,8 +6,8 @@ import ( "net/http" "strings" - httphelper "github.com/caos/oidc/pkg/http" - "github.com/caos/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" ) type UserinfoProvider interface { diff --git a/pkg/op/verifier_access_token.go b/pkg/op/verifier_access_token.go index 2220244..2f19e91 100644 --- a/pkg/op/verifier_access_token.go +++ b/pkg/op/verifier_access_token.go @@ -4,7 +4,7 @@ import ( "context" "time" - "github.com/caos/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/oidc" ) type AccessTokenVerifier interface { diff --git a/pkg/op/verifier_id_token_hint.go b/pkg/op/verifier_id_token_hint.go index 7baa075..bd9ff8e 100644 --- a/pkg/op/verifier_id_token_hint.go +++ b/pkg/op/verifier_id_token_hint.go @@ -4,7 +4,7 @@ import ( "context" "time" - "github.com/caos/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/oidc" ) type IDTokenHintVerifier interface { diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index e7784b5..efaec95 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -8,7 +8,7 @@ import ( "gopkg.in/square/go-jose.v2" - "github.com/caos/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/oidc" ) type JWTProfileVerifier interface { From 86fd502434f21a60c7cdd19ba8cee72ac0fd5ad4 Mon Sep 17 00:00:00 2001 From: James Batt Date: Mon, 9 May 2022 23:06:54 +1000 Subject: [PATCH 103/502] feat(op): implemented support for client_credentials grant (#172) * implemented support for client_credentials grant * first draft * Update pkg/op/token_client_credentials.go Co-authored-by: Livio Amstutz * updated placeholder interface name * updated import paths * ran mockgen Co-authored-by: Livio Amstutz --- pkg/oidc/grants/client_credentials.go | 4 +- pkg/oidc/token_request.go | 10 +++ pkg/op/config.go | 1 + pkg/op/discovery.go | 3 + pkg/op/mock/authorizer.mock.go | 2 +- pkg/op/mock/client.mock.go | 2 +- pkg/op/mock/configuration.mock.go | 16 +++- pkg/op/mock/storage.mock.go | 2 +- pkg/op/op.go | 5 ++ pkg/op/storage.go | 4 + pkg/op/token_client_credentials.go | 115 ++++++++++++++++++++++++++ pkg/op/token_request.go | 6 ++ 12 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 pkg/op/token_client_credentials.go diff --git a/pkg/oidc/grants/client_credentials.go b/pkg/oidc/grants/client_credentials.go index 998dda1..e9f467e 100644 --- a/pkg/oidc/grants/client_credentials.go +++ b/pkg/oidc/grants/client_credentials.go @@ -14,7 +14,7 @@ type clientCredentialsGrant struct { } //ClientCredentialsGrantBasic creates an oauth2 `Client Credentials` Grant -//sneding client_id and client_secret as basic auth header +//sending client_id and client_secret as basic auth header func ClientCredentialsGrantBasic(scopes ...string) *clientCredentialsGrantBasic { return &clientCredentialsGrantBasic{ grantType: "client_credentials", @@ -23,7 +23,7 @@ func ClientCredentialsGrantBasic(scopes ...string) *clientCredentialsGrantBasic } //ClientCredentialsGrantValues creates an oauth2 `Client Credentials` Grant -//sneding client_id and client_secret as form values +//sending client_id and client_secret as form values func ClientCredentialsGrantValues(clientID, clientSecret string, scopes ...string) *clientCredentialsGrant { return &clientCredentialsGrant{ clientCredentialsGrantBasic: ClientCredentialsGrantBasic(scopes...), diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index f260f32..c5396da 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -15,6 +15,9 @@ const ( //GrantTypeRefreshToken defines the grant_type `refresh_token` used for the Token Request in the Refresh Token Flow GrantTypeRefreshToken GrantType = "refresh_token" + //GrantTypeClientCredentials defines the grant_type `client_credentials` used for the Token Request in the Client Credentials Token Flow + GrantTypeClientCredentials GrantType = "client_credentials" + //GrantTypeBearer defines the grant_type `urn:ietf:params:oauth:grant-type:jwt-bearer` used for the JWT Authorization Grant GrantTypeBearer GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" @@ -198,3 +201,10 @@ type TokenExchangeRequest struct { Scope SpaceDelimitedArray `schema:"scope"` requestedTokenType string `schema:"requested_token_type"` } + +type ClientCredentialsRequest struct { + GrantType GrantType `schema:"grant_type"` + Scope SpaceDelimitedArray `schema:"scope"` + ClientID string `schema:"client_id"` + ClientSecret string `schema:"client_secret"` +} diff --git a/pkg/op/config.go b/pkg/op/config.go index 527e134..8882964 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -27,6 +27,7 @@ type Configuration interface { GrantTypeRefreshTokenSupported() bool GrantTypeTokenExchangeSupported() bool GrantTypeJWTAuthorizationSupported() bool + GrantTypeClientCredentialsSupported() bool IntrospectionAuthMethodPrivateKeyJWTSupported() bool IntrospectionEndpointSigningAlgorithmsSupported() []string RevocationAuthMethodPrivateKeyJWTSupported() bool diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 6aca120..c06f9a2 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -75,6 +75,9 @@ func GrantTypes(c Configuration) []oidc.GrantType { if c.GrantTypeRefreshTokenSupported() { grantTypes = append(grantTypes, oidc.GrantTypeRefreshToken) } + if c.GrantTypeClientCredentialsSupported() { + grantTypes = append(grantTypes, oidc.GrantTypeClientCredentials) + } if c.GrantTypeTokenExchangeSupported() { grantTypes = append(grantTypes, oidc.GrantTypeTokenExchange) } diff --git a/pkg/op/mock/authorizer.mock.go b/pkg/op/mock/authorizer.mock.go index 898522f..52f3877 100644 --- a/pkg/op/mock/authorizer.mock.go +++ b/pkg/op/mock/authorizer.mock.go @@ -7,9 +7,9 @@ package mock import ( reflect "reflect" + gomock "github.com/golang/mock/gomock" http "github.com/zitadel/oidc/pkg/http" op "github.com/zitadel/oidc/pkg/op" - gomock "github.com/golang/mock/gomock" ) // MockAuthorizer is a mock of Authorizer interface. diff --git a/pkg/op/mock/client.mock.go b/pkg/op/mock/client.mock.go index 46eb674..cfe3703 100644 --- a/pkg/op/mock/client.mock.go +++ b/pkg/op/mock/client.mock.go @@ -8,9 +8,9 @@ import ( reflect "reflect" time "time" + gomock "github.com/golang/mock/gomock" oidc "github.com/zitadel/oidc/pkg/oidc" op "github.com/zitadel/oidc/pkg/op" - gomock "github.com/golang/mock/gomock" ) // MockClient is a mock of Client interface. diff --git a/pkg/op/mock/configuration.mock.go b/pkg/op/mock/configuration.mock.go index 55c13af..e0c90dc 100644 --- a/pkg/op/mock/configuration.mock.go +++ b/pkg/op/mock/configuration.mock.go @@ -7,8 +7,8 @@ package mock import ( reflect "reflect" - op "github.com/zitadel/oidc/pkg/op" gomock "github.com/golang/mock/gomock" + op "github.com/zitadel/oidc/pkg/op" language "golang.org/x/text/language" ) @@ -105,6 +105,20 @@ func (mr *MockConfigurationMockRecorder) EndSessionEndpoint() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EndSessionEndpoint", reflect.TypeOf((*MockConfiguration)(nil).EndSessionEndpoint)) } +// GrantTypeClientCredentialsSupported mocks base method. +func (m *MockConfiguration) GrantTypeClientCredentialsSupported() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GrantTypeClientCredentialsSupported") + ret0, _ := ret[0].(bool) + return ret0 +} + +// GrantTypeClientCredentialsSupported indicates an expected call of GrantTypeClientCredentialsSupported. +func (mr *MockConfigurationMockRecorder) GrantTypeClientCredentialsSupported() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeClientCredentialsSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeClientCredentialsSupported)) +} + // GrantTypeJWTAuthorizationSupported mocks base method. func (m *MockConfiguration) GrantTypeJWTAuthorizationSupported() bool { m.ctrl.T.Helper() diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index e8743d4..785a643 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -9,9 +9,9 @@ import ( reflect "reflect" time "time" + gomock "github.com/golang/mock/gomock" oidc "github.com/zitadel/oidc/pkg/oidc" op "github.com/zitadel/oidc/pkg/op" - gomock "github.com/golang/mock/gomock" jose "gopkg.in/square/go-jose.v2" ) diff --git a/pkg/op/op.go b/pkg/op/op.go index 99afca3..0d3bc76 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -229,6 +229,11 @@ func (o *openidProvider) GrantTypeJWTAuthorizationSupported() bool { return true } +func (o *openidProvider) GrantTypeClientCredentialsSupported() bool { + _, ok := o.storage.(ClientCredentialsStorage) + return ok +} + func (o *openidProvider) IntrospectionAuthMethodPrivateKeyJWTSupported() bool { return true } diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 710a7dd..1b90a98 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -27,6 +27,10 @@ type AuthStorage interface { GetKeySet(context.Context) (*jose.JSONWebKeySet, error) } +type ClientCredentialsStorage interface { + ClientCredentialsTokenRequest(ctx context.Context, clientID string, scopes []string) (TokenRequest, error) +} + type OPStorage interface { GetClientByClientID(ctx context.Context, clientID string) (Client, error) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error diff --git a/pkg/op/token_client_credentials.go b/pkg/op/token_client_credentials.go new file mode 100644 index 0000000..2248654 --- /dev/null +++ b/pkg/op/token_client_credentials.go @@ -0,0 +1,115 @@ +package op + +import ( + "context" + "net/http" + "net/url" + + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" +) + +//ClientCredentialsExchange handles the OAuth 2.0 client_credentials grant, including +//parsing, validating, authorizing the client and finally returning a token +func ClientCredentialsExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { + request, err := ParseClientCredentialsRequest(r, exchanger.Decoder()) + if err != nil { + RequestError(w, r, err) + } + + validatedRequest, client, err := ValidateClientCredentialsRequest(r.Context(), request, exchanger) + if err != nil { + RequestError(w, r, err) + return + } + + resp, err := CreateClientCredentialsTokenResponse(r.Context(), validatedRequest, exchanger, client) + if err != nil { + RequestError(w, r, err) + return + } + + httphelper.MarshalJSON(w, resp) +} + +//ParseClientCredentialsRequest parsed the http request into a oidc.ClientCredentialsRequest +func ParseClientCredentialsRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.ClientCredentialsRequest, error) { + err := r.ParseForm() + if err != nil { + return nil, oidc.ErrInvalidRequest().WithDescription("error parsing form").WithParent(err) + } + + request := new(oidc.ClientCredentialsRequest) + err = decoder.Decode(request, r.Form) + if err != nil { + return nil, oidc.ErrInvalidRequest().WithDescription("error decoding form").WithParent(err) + } + + if clientID, clientSecret, ok := r.BasicAuth(); ok { + clientID, err = url.QueryUnescape(clientID) + if err != nil { + return nil, oidc.ErrInvalidClient().WithDescription("invalid basic auth header").WithParent(err) + } + + clientSecret, err = url.QueryUnescape(clientSecret) + if err != nil { + return nil, oidc.ErrInvalidClient().WithDescription("invalid basic auth header").WithParent(err) + } + + request.ClientID = clientID + request.ClientSecret = clientSecret + } + + return request, nil +} + +//ValidateClientCredentialsRequest validates the refresh_token request parameters including authorization check of the client +//and returns the data representing the original auth request corresponding to the refresh_token +func ValidateClientCredentialsRequest(ctx context.Context, request *oidc.ClientCredentialsRequest, exchanger Exchanger) (TokenRequest, Client, error) { + storage, ok := exchanger.Storage().(ClientCredentialsStorage) + if !ok { + return nil, nil, oidc.ErrUnsupportedGrantType().WithDescription("client_credentials grant not supported") + } + + client, err := AuthorizeClientCredentialsClient(ctx, request, exchanger) + if err != nil { + return nil, nil, err + } + + tokenRequest, err := storage.ClientCredentialsTokenRequest(ctx, request.ClientID, request.Scope) + if err != nil { + return nil, nil, err + } + + return tokenRequest, client, nil +} + +func AuthorizeClientCredentialsClient(ctx context.Context, request *oidc.ClientCredentialsRequest, exchanger Exchanger) (Client, error) { + if err := AuthorizeClientIDSecret(ctx, request.ClientID, request.ClientSecret, exchanger.Storage()); err != nil { + return nil, err + } + + client, err := exchanger.Storage().GetClientByClientID(ctx, request.ClientID) + if err != nil { + return nil, oidc.ErrInvalidClient().WithParent(err) + } + + if !ValidateGrantType(client, oidc.GrantTypeClientCredentials) { + return nil, oidc.ErrUnauthorizedClient() + } + + return client, nil +} + +func CreateClientCredentialsTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator, client Client) (*oidc.AccessTokenResponse, error) { + accessToken, _, validity, err := CreateAccessToken(ctx, tokenRequest, AccessTokenTypeJWT, creator, client, "") + if err != nil { + return nil, err + } + + return &oidc.AccessTokenResponse{ + AccessToken: accessToken, + TokenType: oidc.BearerToken, + ExpiresIn: uint64(validity.Seconds()), + }, nil +} diff --git a/pkg/op/token_request.go b/pkg/op/token_request.go index 712edca..71bf077 100644 --- a/pkg/op/token_request.go +++ b/pkg/op/token_request.go @@ -20,6 +20,7 @@ type Exchanger interface { GrantTypeRefreshTokenSupported() bool GrantTypeTokenExchangeSupported() bool GrantTypeJWTAuthorizationSupported() bool + GrantTypeClientCredentialsSupported() bool } func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Request) { @@ -44,6 +45,11 @@ func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Reque TokenExchange(w, r, exchanger) return } + case string(oidc.GrantTypeClientCredentials): + if exchanger.GrantTypeClientCredentialsSupported() { + ClientCredentialsExchange(w, r, exchanger) + return + } case "": RequestError(w, r, oidc.ErrInvalidRequest().WithDescription("grant_type missing")) return From ff124f87f57ce68437f119801f4c1b0385079997 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Wed, 11 May 2022 10:19:16 +0200 Subject: [PATCH 104/502] docs(readme): update features and add contributors (#180) --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d71ff13..9b456c1 100644 --- a/README.md +++ b/README.md @@ -55,10 +55,18 @@ CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998/ SCOPES="openid ## Features -| | Code Flow | Implicit Flow | Hybrid Flow | Discovery | PKCE | Token Exchange | mTLS | JWT Profile | Refresh Token | -|------------------|-----------|---------------|-------------|-----------|------|----------------|---------|-------------|---------------| -| Relying Party | yes | no[^1] | no | yes | yes | partial | not yet | yes | yes | -| OpenID Provider | yes | yes | not yet | yes | yes | not yet | not yet | yes | yes | +| | Code Flow | Implicit Flow | Hybrid Flow | Discovery | PKCE | Token Exchange | mTLS | JWT Profile | Refresh Token | Client Credentials | +|------------------|-----------|---------------|-------------|-----------|------|----------------|---------|-------------|---------------|--------------------| +| Relying Party | yes | no[^1] | no | yes | yes | partial | not yet | yes | yes | not yet | +| OpenID Provider | yes | yes | not yet | yes | yes | not yet | not yet | yes | yes | yes | + +## Contributors + + + + + +Made with [contrib.rocks](https://contrib.rocks). ### Resources From 9b0954f3d4d696014d937421417541c0f8aad16c Mon Sep 17 00:00:00 2001 From: Jederson Zuchi Date: Fri, 13 May 2022 04:17:20 -0300 Subject: [PATCH 105/502] feat(rp): Adding end_session endpoint to relaying party interface (#179) --- pkg/client/rp/relaying_party.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/client/rp/relaying_party.go b/pkg/client/rp/relaying_party.go index 610a903..5fbce27 100644 --- a/pkg/client/rp/relaying_party.go +++ b/pkg/client/rp/relaying_party.go @@ -50,6 +50,9 @@ type RelyingParty interface { //Signer is used if the relaying party uses the JWT Profile Signer() jose.Signer + //GetEndSessionEndpoint returns the endpoint to sign out on a IDP + GetEndSessionEndpoint() string + //UserinfoEndpoint returns the userinfo UserinfoEndpoint() string @@ -117,6 +120,10 @@ func (rp *relyingParty) UserinfoEndpoint() string { return rp.endpoints.UserinfoURL } +func (rp *relyingParty) GetEndSessionEndpoint() string { + return rp.endpoints.EndSessionURL +} + func (rp *relyingParty) IDTokenVerifier() IDTokenVerifier { if rp.idTokenVerifier == nil { rp.idTokenVerifier = NewIDTokenVerifier(rp.issuer, rp.oauthConfig.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...) @@ -476,6 +483,7 @@ type Endpoints struct { IntrospectURL string UserinfoURL string JKWsURL string + EndSessionURL string } func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints { @@ -488,6 +496,7 @@ func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints { IntrospectURL: discoveryConfig.IntrospectionEndpoint, UserinfoURL: discoveryConfig.UserinfoEndpoint, JKWsURL: discoveryConfig.JwksURI, + EndSessionURL: discoveryConfig.EndSessionEndpoint, } } From c4812dd8de8fe0d5b27a0b65c633cbbcd669dda2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jun 2022 07:24:22 +0200 Subject: [PATCH 106/502] chore(deps): bump github.com/stretchr/testify from 1.7.1 to 1.7.4 (#186) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.1 to 1.7.4. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.7.1...v1.7.4) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 570a3ba..d313764 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/gorilla/securecookie v1.1.1 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/sirupsen/logrus v1.8.1 - github.com/stretchr/testify v1.7.1 + github.com/stretchr/testify v1.7.4 github.com/zitadel/logging v0.3.3 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 golang.org/x/text v0.3.7 diff --git a/go.sum b/go.sum index 92f343e..f887641 100644 --- a/go.sum +++ b/go.sum @@ -134,11 +134,13 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM= +github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 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= @@ -405,8 +407,9 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From 854e14b7c46456cc86990ee3425c9c9b9d4ca125 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Tue, 21 Jun 2022 07:24:40 +0200 Subject: [PATCH 107/502] fix: state and auth code response encoding (#185) * fix: add state in access token response (implicit flow) * fix: encode auth response correctly (when using query in redirect uri) * fix query param handling --- pkg/http/http.go | 7 ++-- pkg/oidc/token.go | 1 + pkg/op/auth_request.go | 35 +++++++++++++--- pkg/op/auth_request_test.go | 84 +++++++++++++++++++++++++++++++++++++ pkg/op/token.go | 3 ++ 5 files changed, 120 insertions(+), 10 deletions(-) diff --git a/pkg/http/http.go b/pkg/http/http.go index 2512707..b0d1ef7 100644 --- a/pkg/http/http.go +++ b/pkg/http/http.go @@ -77,14 +77,13 @@ func HttpRequest(client *http.Client, req *http.Request, response interface{}) e return nil } -func URLEncodeResponse(resp interface{}, encoder Encoder) (string, error) { +func URLEncodeParams(resp interface{}, encoder Encoder) (url.Values, error) { values := make(map[string][]string) err := encoder.Encode(resp, values) if err != nil { - return "", err + return nil, err } - v := url.Values(values) - return v.Encode(), nil + return values, nil } func StartServer(ctx context.Context, port string) { diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index 7b5b812..9c8c48b 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -396,6 +396,7 @@ type AccessTokenResponse struct { RefreshToken string `json:"refresh_token,omitempty" schema:"refresh_token,omitempty"` ExpiresIn uint64 `json:"expires_in,omitempty" schema:"expires_in,omitempty"` IDToken string `json:"id_token,omitempty" schema:"id_token,omitempty"` + State string `json:"state,omitempty" schema:"state,omitempty"` } type JWTProfileAssertionClaims interface { diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 8a2b8b9..2ebedb5 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -465,18 +465,41 @@ func BuildAuthRequestCode(authReq AuthRequest, crypto Crypto) (string, error) { //AuthResponseURL encodes the authorization response (successful and error) and sets it as query or fragment values //depending on the response_mode and response_type func AuthResponseURL(redirectURI string, responseType oidc.ResponseType, responseMode oidc.ResponseMode, response interface{}, encoder httphelper.Encoder) (string, error) { - params, err := httphelper.URLEncodeResponse(response, encoder) + uri, err := url.Parse(redirectURI) if err != nil { return "", oidc.ErrServerError().WithParent(err) } + params, err := httphelper.URLEncodeParams(response, encoder) + if err != nil { + return "", oidc.ErrServerError().WithParent(err) + } + //return explicitly requested mode if responseMode == oidc.ResponseModeQuery { - return redirectURI + "?" + params, nil + return mergeQueryParams(uri, params), nil } if responseMode == oidc.ResponseModeFragment { - return redirectURI + "#" + params, nil + return setFragment(uri, params), nil } - if responseType == "" || responseType == oidc.ResponseTypeCode { - return redirectURI + "?" + params, nil + //implicit must use fragment mode is not specified by client + if responseType == oidc.ResponseTypeIDToken || responseType == oidc.ResponseTypeIDTokenOnly { + return setFragment(uri, params), nil } - return redirectURI + "#" + params, nil + //if we get here it's code flow: defaults to query + return mergeQueryParams(uri, params), nil +} + +func setFragment(uri *url.URL, params url.Values) string { + uri.Fragment = params.Encode() + return uri.String() +} + +func mergeQueryParams(uri *url.URL, params url.Values) string { + queries := uri.Query() + for param, values := range params { + for _, value := range values { + queries.Add(param, value) + } + } + uri.RawQuery = queries.Encode() + return uri.String() } diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index f9ba4de..9023011 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -793,6 +793,90 @@ func TestAuthResponseURL(t *testing.T) { nil, }, }, + { + "with query", + args{ + "uri?param=value", + oidc.ResponseTypeCode, + "", + map[string][]string{"test": {"test"}}, + &mockEncoder{}, + }, + res{ + "uri?param=value&test=test", + nil, + }, + }, + { + "with query response type id token", + args{ + "uri?param=value", + oidc.ResponseTypeIDToken, + "", + map[string][]string{"test": {"test"}}, + &mockEncoder{}, + }, + res{ + "uri?param=value#test=test", + nil, + }, + }, + { + "with existing query", + args{ + "uri?test=value", + oidc.ResponseTypeCode, + "", + map[string][]string{"test": {"test"}}, + &mockEncoder{}, + }, + res{ + "uri?test=value&test=test", + nil, + }, + }, + { + "with existing query response type id token", + args{ + "uri?test=value", + oidc.ResponseTypeIDToken, + "", + map[string][]string{"test": {"test"}}, + &mockEncoder{}, + }, + res{ + "uri?test=value#test=test", + nil, + }, + }, + { + "with existing query and multiple values", + args{ + "uri?test=value", + oidc.ResponseTypeCode, + "", + map[string][]string{"test": {"test", "test2"}}, + &mockEncoder{}, + }, + res{ + "uri?test=value&test=test&test=test2", + nil, + }, + }, + { + "with existing query and multiple values response type id token", + args{ + "uri?test=value", + oidc.ResponseTypeIDToken, + "", + map[string][]string{"test": {"test", "test2"}}, + &mockEncoder{}, + }, + res{ + "uri?test=value#test=test&test=test2", + nil, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/op/token.go b/pkg/op/token.go index 7f6f599..3a72261 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -37,11 +37,13 @@ func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Cli return nil, err } + var state string if authRequest, ok := request.(AuthRequest); ok { err = creator.Storage().DeleteAuthRequest(ctx, authRequest.GetID()) if err != nil { return nil, err } + state = authRequest.GetState() } exp := uint64(validity.Seconds()) @@ -51,6 +53,7 @@ func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Cli RefreshToken: newRefreshToken, TokenType: oidc.BearerToken, ExpiresIn: exp, + State: state, }, nil } From 9f36a5a3a919fdb81d847cac2de84d21997e7958 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Wed, 29 Jun 2022 02:37:21 -0700 Subject: [PATCH 108/502] fix typo in filename (#188) --- pkg/client/rp/{relaying_party.go => relying_party.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pkg/client/rp/{relaying_party.go => relying_party.go} (100%) diff --git a/pkg/client/rp/relaying_party.go b/pkg/client/rp/relying_party.go similarity index 100% rename from pkg/client/rp/relaying_party.go rename to pkg/client/rp/relying_party.go From c4d951cad2a324180ed202c814af2f1f2410f19b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jun 2022 11:39:29 +0200 Subject: [PATCH 109/502] chore(deps): bump github.com/stretchr/testify from 1.7.4 to 1.7.5 (#187) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.4 to 1.7.5. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.7.4...v1.7.5) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d313764..77ed81f 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/gorilla/securecookie v1.1.1 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/sirupsen/logrus v1.8.1 - github.com/stretchr/testify v1.7.4 + github.com/stretchr/testify v1.7.5 github.com/zitadel/logging v0.3.3 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 golang.org/x/text v0.3.7 diff --git a/go.sum b/go.sum index f887641..48d41e0 100644 --- a/go.sum +++ b/go.sum @@ -139,8 +139,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM= -github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q= +github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 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= From 385d5c15dab8953bacb94fbc894728edc0c3cbdc Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Wed, 29 Jun 2022 02:39:32 -0700 Subject: [PATCH 110/502] define GrantType constants in one place (#189) --- pkg/oidc/discovery.go | 4 ---- pkg/oidc/token_request.go | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go index 1a92f8d..673d628 100644 --- a/pkg/oidc/discovery.go +++ b/pkg/oidc/discovery.go @@ -157,7 +157,3 @@ const ( AuthMethodNone AuthMethod = "none" AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt" ) - -const ( - GrantTypeImplicit GrantType = "implicit" -) diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index c5396da..56c4f1c 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -24,6 +24,9 @@ const ( //GrantTypeTokenExchange defines the grant_type `urn:ietf:params:oauth:grant-type:token-exchange` used for the OAuth Token Exchange Grant GrantTypeTokenExchange GrantType = "urn:ietf:params:oauth:grant-type:token-exchange" + //GrantTypeImplicit defines the grant type `implicit` used for implicit flows that skip the generation and exchange of an Authorization Code + GrantTypeImplicit GrantType = "implicit" + //ClientAssertionTypeJWTAssertion defines the client_assertion_type `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` //used for the OAuth JWT Profile Client Authentication ClientAssertionTypeJWTAssertion = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" From fb0c466839442560609d33375e816328b58e7e61 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Thu, 30 Jun 2022 04:20:18 -0700 Subject: [PATCH 111/502] chore: add doc links (#190) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9b456c1..1c9e30d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![Release](https://github.com/zitadel/oidc/workflows/Release/badge.svg)](https://github.com/zitadel/oidc/actions) +[![GoDoc](https://godoc.org/github.com/zitadel/oidc?status.png)](https://pkg.go.dev/github.com/zitadel/oidc) [![license](https://badgen.net/github/license/zitadel/oidc/)](https://github.com/zitadel/oidc/blob/master/LICENSE) [![release](https://badgen.net/github/release/zitadel/oidc/stable)](https://github.com/zitadel/oidc/releases) [![Go Report Card](https://goreportcard.com/badge/github.com/zitadel/oidc)](https://goreportcard.com/report/github.com/zitadel/oidc) @@ -77,6 +78,7 @@ For your convenience you can find the relevant standards linked below. - [OAuth 2.0 Token Exchange](https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-19) - [OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens](https://tools.ietf.org/html/draft-ietf-oauth-mtls-17) - [JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants](https://tools.ietf.org/html/rfc7523) +- [OIDC/OAuth Flow in Zitadel (using this library)](https://docs.zitadel.com/docs/guides/authentication/login-users) ## Supported Go Versions From 498b70bae1a3a66b51bb42404a4e35f668e819ec Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Mon, 4 Jul 2022 00:20:29 -0700 Subject: [PATCH 112/502] chore: add some docs to NewOpenIDProvider() (#191) * add some docs to NewOpenIDProvider() * typo --- pkg/op/op.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pkg/op/op.go b/pkg/op/op.go index 0d3bc76..5b8567a 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -117,6 +117,23 @@ type endpoints struct { JwksURI Endpoint } +//NewOpenIDProvider creates a provider. The provider provides (with HttpHandler()) +//a http.Router that handles a suite of endpoints (some paths can be overridden): +// /healthz +// /ready +// /.well-known/openid-configuration +// /oauth/token +// /oauth/introspect +// /callback +// /authorize +// /userinfo +// /revoke +// /end_session +// /keys +//This does not include login. Login is handled with a redirect that includes the +//request ID. The redirect for logins is specified per-client by Client.LoginURL(). +//Successful logins should mark the request as authorized and redirect back to to +//op.AuthCallbackURL(provider) which is probably /callback. func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opOpts ...Option) (OpenIDProvider, error) { err := ValidateIssuer(config.Issuer) if err != nil { From aea3f4326805fba6cc5b62b6a05d8d2d97fee1d2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Jul 2022 09:21:02 +0200 Subject: [PATCH 113/502] chore(deps): bump github.com/stretchr/testify from 1.7.5 to 1.8.0 (#192) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.5 to 1.8.0. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.7.5...v1.8.0) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 77ed81f..81b6217 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/gorilla/securecookie v1.1.1 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/sirupsen/logrus v1.8.1 - github.com/stretchr/testify v1.7.5 + github.com/stretchr/testify v1.8.0 github.com/zitadel/logging v0.3.3 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 golang.org/x/text v0.3.7 diff --git a/go.sum b/go.sum index 48d41e0..8146afe 100644 --- a/go.sum +++ b/go.sum @@ -139,8 +139,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q= -github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 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= From 292b0cc9f9cc56186008b6d5271ce3a70533ea4b Mon Sep 17 00:00:00 2001 From: mffap Date: Wed, 20 Jul 2022 15:31:30 +0200 Subject: [PATCH 114/502] chore: update website (#195) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1c9e30d..872b9b6 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ We did not choose `fosite` because it implements `OAuth 2.0` on its own and does ## License -The full functionality of this library is and stays open source and free to use for everyone. Visit our [website](https://caos.ch) and get in touch. +The full functionality of this library is and stays open source and free to use for everyone. Visit our [website](https://zitadel.com) and get in touch. See the exact licensing terms [here](./LICENSE) From 8dd5c87faa05efee0fe5e6d670346cfeff274aa2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Jul 2022 15:31:52 +0200 Subject: [PATCH 115/502] chore(deps): bump github.com/sirupsen/logrus from 1.8.1 to 1.9.0 (#196) Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.8.1 to 1.9.0. - [Release notes](https://github.com/sirupsen/logrus/releases) - [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md) - [Commits](https://github.com/sirupsen/logrus/compare/v1.8.1...v1.9.0) --- updated-dependencies: - dependency-name: github.com/sirupsen/logrus dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 81b6217..0ed79a6 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/gorilla/schema v1.2.0 github.com/gorilla/securecookie v1.1.1 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect - github.com/sirupsen/logrus v1.8.1 + github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.0 github.com/zitadel/logging v0.3.3 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 diff --git a/go.sum b/go.sum index 8146afe..87b2fc1 100644 --- a/go.sum +++ b/go.sum @@ -131,8 +131,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -262,8 +263,9 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220207234003-57398862261d h1:Bm7BNOQt2Qv7ZqysjeLjgCBanX+88Z/OtdvsrEv1Djc= golang.org/x/sys v0.0.0-20220207234003-57398862261d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 5fb36bf4c252dedf52cdd655499fdf376fa536d9 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Wed, 20 Jul 2022 06:36:17 -0700 Subject: [PATCH 116/502] fix: Add db scanner methods for SpaceDelimitedArray (#194) --- pkg/oidc/token_request.go | 5 +++++ pkg/oidc/types.go | 30 ++++++++++++++++++++++++++++ pkg/oidc/types_test.go | 41 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index 56c4f1c..989e792 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -32,6 +32,11 @@ const ( ClientAssertionTypeJWTAssertion = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" ) +var AllGrantTypes = []GrantType{ + GrantTypeCode, GrantTypeRefreshToken, GrantTypeClientCredentials, + GrantTypeBearer, GrantTypeTokenExchange, GrantTypeImplicit, + ClientAssertionTypeJWTAssertion} + type GrantType string type TokenRequest interface { diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index b6a75f4..1260798 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -1,7 +1,9 @@ package oidc import ( + "database/sql/driver" "encoding/json" + "fmt" "strings" "time" @@ -95,6 +97,34 @@ func (s *SpaceDelimitedArray) UnmarshalJSON(data []byte) error { return nil } +func (s *SpaceDelimitedArray) Scan(src interface{}) error { + if src == nil { + *s = nil + return nil + } + switch v := src.(type) { + case string: + if len(v) == 0 { + *s = SpaceDelimitedArray{} + return nil + } + *s = strings.Split(v, " ") + case []byte: + if len(v) == 0 { + *s = SpaceDelimitedArray{} + return nil + } + *s = strings.Split(string(v), " ") + default: + return fmt.Errorf("cannot convert %T to SpaceDelimitedArray", src) + } + return nil +} + +func (s SpaceDelimitedArray) Value() (driver.Value, error) { + return strings.Join(s, " "), nil +} + type Time time.Time func (t *Time) UnmarshalJSON(data []byte) error { diff --git a/pkg/oidc/types_test.go b/pkg/oidc/types_test.go index c03a775..6c62c40 100644 --- a/pkg/oidc/types_test.go +++ b/pkg/oidc/types_test.go @@ -3,6 +3,8 @@ package oidc import ( "bytes" "encoding/json" + "strconv" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -228,6 +230,7 @@ func TestScopes_UnmarshalText(t *testing.T) { }) } } + func TestScopes_MarshalText(t *testing.T) { type args struct { scopes SpaceDelimitedArray @@ -294,3 +297,41 @@ func TestScopes_MarshalText(t *testing.T) { }) } } + +func TestSpaceDelimitatedArray_ValuerNotNil(t *testing.T) { + inputs := [][]string{ + {"two", "elements"}, + {"one"}, + { /*zero*/ }, + } + for _, input := range inputs { + t.Run(strconv.Itoa(len(input))+strings.Join(input, "_"), func(t *testing.T) { + sda := SpaceDelimitedArray(input) + dbValue, err := sda.Value() + if !assert.NoError(t, err, "Value") { + return + } + var reversed SpaceDelimitedArray + err = reversed.Scan(dbValue) + if assert.NoError(t, err, "Scan string") { + assert.Equal(t, sda, reversed, "scan string") + } + reversed = nil + dbValueString, ok := dbValue.(string) + if assert.True(t, ok, "dbValue is string") { + err = reversed.Scan([]byte(dbValueString)) + if assert.NoError(t, err, "Scan bytes") { + assert.Equal(t, sda, reversed, "scan bytes") + } + } + }) + } +} + +func TestSpaceDelimitatedArray_ValuerNil(t *testing.T) { + var reversed SpaceDelimitedArray + err := reversed.Scan(nil) + if assert.NoError(t, err, "Scan nil") { + assert.Equal(t, SpaceDelimitedArray(nil), reversed, "scan nil") + } +} From 653209a23ceb64bb46ddd4572fa9af3cf03620a4 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Thu, 21 Jul 2022 09:34:14 +0200 Subject: [PATCH 117/502] feat: add all optional claims of the introspection response --- pkg/oidc/introspection.go | 86 +++++++++++++++++++++++++++++++-------- 1 file changed, 70 insertions(+), 16 deletions(-) diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index 6ac2986..33ba2cb 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -19,10 +19,17 @@ type ClientAssertionParams struct { type IntrospectionResponse interface { UserInfoSetter - SetActive(bool) IsActive() bool + SetActive(bool) SetScopes(scopes []string) SetClientID(id string) + SetTokenType(tokenType string) + SetExpiration(exp time.Time) + SetIssuedAt(iat time.Time) + SetNotBefore(nbf time.Time) + SetAudience(audience []string) + SetIssuer(issuer string) + SetJWTID(id string) } func NewIntrospectionResponse() IntrospectionResponse { @@ -30,10 +37,17 @@ func NewIntrospectionResponse() IntrospectionResponse { } type introspectionResponse struct { - Active bool `json:"active"` - Scope SpaceDelimitedArray `json:"scope,omitempty"` - ClientID string `json:"client_id,omitempty"` - Subject string `json:"sub,omitempty"` + Active bool `json:"active"` + Scope SpaceDelimitedArray `json:"scope,omitempty"` + ClientID string `json:"client_id,omitempty"` + TokenType string `json:"token_type,omitempty"` + Expiration Time `json:"exp,omitempty"` + IssuedAt Time `json:"iat,omitempty"` + NotBefore Time `json:"nbf,omitempty"` + Subject string `json:"sub,omitempty"` + Audience Audience `json:"aud,omitempty"` + Issuer string `json:"iss,omitempty"` + JWTID string `json:"jti,omitempty"` userInfoProfile userInfoEmail userInfoPhone @@ -46,14 +60,6 @@ func (i *introspectionResponse) IsActive() bool { return i.Active } -func (i *introspectionResponse) SetScopes(scope []string) { - i.Scope = scope -} - -func (i *introspectionResponse) SetClientID(id string) { - i.ClientID = id -} - func (i *introspectionResponse) GetSubject() string { return i.Subject } @@ -138,6 +144,42 @@ func (i *introspectionResponse) SetActive(active bool) { i.Active = active } +func (i *introspectionResponse) SetScopes(scope []string) { + i.Scope = scope +} + +func (i *introspectionResponse) SetClientID(id string) { + i.ClientID = id +} + +func (i *introspectionResponse) SetTokenType(tokenType string) { + i.TokenType = tokenType +} + +func (i *introspectionResponse) SetExpiration(exp time.Time) { + i.Expiration = Time(exp) +} + +func (i *introspectionResponse) SetIssuedAt(iat time.Time) { + i.IssuedAt = Time(iat) +} + +func (i *introspectionResponse) SetNotBefore(nbf time.Time) { + i.NotBefore = Time(nbf) +} + +func (i *introspectionResponse) SetAudience(audience []string) { + i.Audience = audience +} + +func (i *introspectionResponse) SetIssuer(issuer string) { + i.Issuer = issuer +} + +func (i *introspectionResponse) SetJWTID(id string) { + i.JWTID = id +} + func (i *introspectionResponse) SetSubject(sub string) { i.Subject = sub } @@ -223,9 +265,12 @@ func (i *introspectionResponse) MarshalJSON() ([]byte, error) { type Alias introspectionResponse a := &struct { *Alias - Locale interface{} `json:"locale,omitempty"` - UpdatedAt int64 `json:"updated_at,omitempty"` - Username string `json:"username,omitempty"` + Expiration int64 `json:"exp,omitempty"` + IssuedAt int64 `json:"iat,omitempty"` + NotBefore int64 `json:"nbf,omitempty"` + Locale interface{} `json:"locale,omitempty"` + UpdatedAt int64 `json:"updated_at,omitempty"` + Username string `json:"username,omitempty"` }{ Alias: (*Alias)(i), } @@ -235,6 +280,15 @@ func (i *introspectionResponse) MarshalJSON() ([]byte, error) { if !time.Time(i.UpdatedAt).IsZero() { a.UpdatedAt = time.Time(i.UpdatedAt).Unix() } + if !time.Time(i.Expiration).IsZero() { + a.Expiration = time.Time(i.Expiration).Unix() + } + if !time.Time(i.IssuedAt).IsZero() { + a.IssuedAt = time.Time(i.IssuedAt).Unix() + } + if !time.Time(i.NotBefore).IsZero() { + a.NotBefore = time.Time(i.NotBefore).Unix() + } a.Username = i.PreferredUsername b, err := json.Marshal(a) From 531caae613117eca7103bd30292567ffcc353bbf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jul 2022 20:00:56 +0200 Subject: [PATCH 118/502] chore(deps): bump github.com/zitadel/logging from 0.3.3 to 0.3.4 (#200) Bumps [github.com/zitadel/logging](https://github.com/zitadel/logging) from 0.3.3 to 0.3.4. - [Release notes](https://github.com/zitadel/logging/releases) - [Changelog](https://github.com/zitadel/logging/blob/main/.releaserc.js) - [Commits](https://github.com/zitadel/logging/compare/v0.3.3...v0.3.4) --- updated-dependencies: - dependency-name: github.com/zitadel/logging dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0ed79a6..6b0db2b 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.0 - github.com/zitadel/logging v0.3.3 + github.com/zitadel/logging v0.3.4 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 golang.org/x/text v0.3.7 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect diff --git a/go.sum b/go.sum index 87b2fc1..57260a4 100644 --- a/go.sum +++ b/go.sum @@ -146,8 +146,8 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/zitadel/logging v0.3.3 h1:/nAoki9HFJK+qMLBVY5Jhbfp/6o3YLK49Tw5j2oRhjM= -github.com/zitadel/logging v0.3.3/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0= +github.com/zitadel/logging v0.3.4 h1:9hZsTjMMTE3X2LUi0xcF9Q9EdLo+FAezeu52ireBbHM= +github.com/zitadel/logging v0.3.4/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0= 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= From b84bcbed76771fe1b70f3b24b51e398ad81856b6 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Mon, 25 Jul 2022 11:06:49 -0700 Subject: [PATCH 119/502] chore: add enumer for iota-defined types (#197) Co-authored-by: Livio Spring --- go.mod | 2 + go.sum | 10 +- pkg/op/applicationtype_enumer.go | 342 +++++++++++++++++++++++++++++++ pkg/op/client.go | 13 +- 4 files changed, 360 insertions(+), 7 deletions(-) create mode 100644 pkg/op/applicationtype_enumer.go diff --git a/go.mod b/go.mod index 6b0db2b..616d0e0 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,8 @@ require ( github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.0 github.com/zitadel/logging v0.3.4 + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect + golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 golang.org/x/text v0.3.7 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect diff --git a/go.sum b/go.sum index 57260a4..68116ea 100644 --- a/go.sum +++ b/go.sum @@ -157,8 +157,9 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -216,8 +217,10 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -262,7 +265,9 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220207234003-57398862261d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -272,6 +277,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/pkg/op/applicationtype_enumer.go b/pkg/op/applicationtype_enumer.go new file mode 100644 index 0000000..7f0b1e0 --- /dev/null +++ b/pkg/op/applicationtype_enumer.go @@ -0,0 +1,342 @@ +// Code generated by "enumer -linecomment -sql -json -text -yaml -gqlgen -type=ApplicationType,AccessTokenType"; DO NOT EDIT. + +package op + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "io" + "strconv" + "strings" +) + +const _ApplicationTypeName = "webuser_agentnative" + +var _ApplicationTypeIndex = [...]uint8{0, 3, 13, 19} + +const _ApplicationTypeLowerName = "webuser_agentnative" + +func (i ApplicationType) String() string { + if i < 0 || i >= ApplicationType(len(_ApplicationTypeIndex)-1) { + return fmt.Sprintf("ApplicationType(%d)", i) + } + return _ApplicationTypeName[_ApplicationTypeIndex[i]:_ApplicationTypeIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _ApplicationTypeNoOp() { + var x [1]struct{} + _ = x[ApplicationTypeWeb-(0)] + _ = x[ApplicationTypeUserAgent-(1)] + _ = x[ApplicationTypeNative-(2)] +} + +var _ApplicationTypeValues = []ApplicationType{ApplicationTypeWeb, ApplicationTypeUserAgent, ApplicationTypeNative} + +var _ApplicationTypeNameToValueMap = map[string]ApplicationType{ + _ApplicationTypeName[0:3]: ApplicationTypeWeb, + _ApplicationTypeLowerName[0:3]: ApplicationTypeWeb, + _ApplicationTypeName[3:13]: ApplicationTypeUserAgent, + _ApplicationTypeLowerName[3:13]: ApplicationTypeUserAgent, + _ApplicationTypeName[13:19]: ApplicationTypeNative, + _ApplicationTypeLowerName[13:19]: ApplicationTypeNative, +} + +var _ApplicationTypeNames = []string{ + _ApplicationTypeName[0:3], + _ApplicationTypeName[3:13], + _ApplicationTypeName[13:19], +} + +// ApplicationTypeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func ApplicationTypeString(s string) (ApplicationType, error) { + if val, ok := _ApplicationTypeNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _ApplicationTypeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to ApplicationType values", s) +} + +// ApplicationTypeValues returns all values of the enum +func ApplicationTypeValues() []ApplicationType { + return _ApplicationTypeValues +} + +// ApplicationTypeStrings returns a slice of all String values of the enum +func ApplicationTypeStrings() []string { + strs := make([]string, len(_ApplicationTypeNames)) + copy(strs, _ApplicationTypeNames) + return strs +} + +// IsAApplicationType returns "true" if the value is listed in the enum definition. "false" otherwise +func (i ApplicationType) IsAApplicationType() bool { + for _, v := range _ApplicationTypeValues { + if i == v { + return true + } + } + return false +} + +// MarshalJSON implements the json.Marshaler interface for ApplicationType +func (i ApplicationType) MarshalJSON() ([]byte, error) { + return json.Marshal(i.String()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface for ApplicationType +func (i *ApplicationType) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return fmt.Errorf("ApplicationType should be a string, got %s", data) + } + + var err error + *i, err = ApplicationTypeString(s) + return err +} + +// MarshalText implements the encoding.TextMarshaler interface for ApplicationType +func (i ApplicationType) MarshalText() ([]byte, error) { + return []byte(i.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for ApplicationType +func (i *ApplicationType) UnmarshalText(text []byte) error { + var err error + *i, err = ApplicationTypeString(string(text)) + return err +} + +// MarshalYAML implements a YAML Marshaler for ApplicationType +func (i ApplicationType) MarshalYAML() (interface{}, error) { + return i.String(), nil +} + +// UnmarshalYAML implements a YAML Unmarshaler for ApplicationType +func (i *ApplicationType) UnmarshalYAML(unmarshal func(interface{}) error) error { + var s string + if err := unmarshal(&s); err != nil { + return err + } + + var err error + *i, err = ApplicationTypeString(s) + return err +} + +func (i ApplicationType) Value() (driver.Value, error) { + return i.String(), nil +} + +func (i *ApplicationType) Scan(value interface{}) error { + if value == nil { + return nil + } + + var str string + switch v := value.(type) { + case []byte: + str = string(v) + case string: + str = v + case fmt.Stringer: + str = v.String() + default: + return fmt.Errorf("invalid value of ApplicationType: %[1]T(%[1]v)", value) + } + + val, err := ApplicationTypeString(str) + if err != nil { + return err + } + + *i = val + return nil +} + +// MarshalGQL implements the graphql.Marshaler interface for ApplicationType +func (i ApplicationType) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(i.String())) +} + +// UnmarshalGQL implements the graphql.Unmarshaler interface for ApplicationType +func (i *ApplicationType) UnmarshalGQL(value interface{}) error { + str, ok := value.(string) + if !ok { + return fmt.Errorf("ApplicationType should be a string, got %T", value) + } + + var err error + *i, err = ApplicationTypeString(str) + return err +} + +const _AccessTokenTypeName = "bearerJWT" + +var _AccessTokenTypeIndex = [...]uint8{0, 6, 9} + +const _AccessTokenTypeLowerName = "bearerjwt" + +func (i AccessTokenType) String() string { + if i < 0 || i >= AccessTokenType(len(_AccessTokenTypeIndex)-1) { + return fmt.Sprintf("AccessTokenType(%d)", i) + } + return _AccessTokenTypeName[_AccessTokenTypeIndex[i]:_AccessTokenTypeIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _AccessTokenTypeNoOp() { + var x [1]struct{} + _ = x[AccessTokenTypeBearer-(0)] + _ = x[AccessTokenTypeJWT-(1)] +} + +var _AccessTokenTypeValues = []AccessTokenType{AccessTokenTypeBearer, AccessTokenTypeJWT} + +var _AccessTokenTypeNameToValueMap = map[string]AccessTokenType{ + _AccessTokenTypeName[0:6]: AccessTokenTypeBearer, + _AccessTokenTypeLowerName[0:6]: AccessTokenTypeBearer, + _AccessTokenTypeName[6:9]: AccessTokenTypeJWT, + _AccessTokenTypeLowerName[6:9]: AccessTokenTypeJWT, +} + +var _AccessTokenTypeNames = []string{ + _AccessTokenTypeName[0:6], + _AccessTokenTypeName[6:9], +} + +// AccessTokenTypeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func AccessTokenTypeString(s string) (AccessTokenType, error) { + if val, ok := _AccessTokenTypeNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _AccessTokenTypeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to AccessTokenType values", s) +} + +// AccessTokenTypeValues returns all values of the enum +func AccessTokenTypeValues() []AccessTokenType { + return _AccessTokenTypeValues +} + +// AccessTokenTypeStrings returns a slice of all String values of the enum +func AccessTokenTypeStrings() []string { + strs := make([]string, len(_AccessTokenTypeNames)) + copy(strs, _AccessTokenTypeNames) + return strs +} + +// IsAAccessTokenType returns "true" if the value is listed in the enum definition. "false" otherwise +func (i AccessTokenType) IsAAccessTokenType() bool { + for _, v := range _AccessTokenTypeValues { + if i == v { + return true + } + } + return false +} + +// MarshalJSON implements the json.Marshaler interface for AccessTokenType +func (i AccessTokenType) MarshalJSON() ([]byte, error) { + return json.Marshal(i.String()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface for AccessTokenType +func (i *AccessTokenType) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return fmt.Errorf("AccessTokenType should be a string, got %s", data) + } + + var err error + *i, err = AccessTokenTypeString(s) + return err +} + +// MarshalText implements the encoding.TextMarshaler interface for AccessTokenType +func (i AccessTokenType) MarshalText() ([]byte, error) { + return []byte(i.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for AccessTokenType +func (i *AccessTokenType) UnmarshalText(text []byte) error { + var err error + *i, err = AccessTokenTypeString(string(text)) + return err +} + +// MarshalYAML implements a YAML Marshaler for AccessTokenType +func (i AccessTokenType) MarshalYAML() (interface{}, error) { + return i.String(), nil +} + +// UnmarshalYAML implements a YAML Unmarshaler for AccessTokenType +func (i *AccessTokenType) UnmarshalYAML(unmarshal func(interface{}) error) error { + var s string + if err := unmarshal(&s); err != nil { + return err + } + + var err error + *i, err = AccessTokenTypeString(s) + return err +} + +func (i AccessTokenType) Value() (driver.Value, error) { + return i.String(), nil +} + +func (i *AccessTokenType) Scan(value interface{}) error { + if value == nil { + return nil + } + + var str string + switch v := value.(type) { + case []byte: + str = string(v) + case string: + str = v + case fmt.Stringer: + str = v.String() + default: + return fmt.Errorf("invalid value of AccessTokenType: %[1]T(%[1]v)", value) + } + + val, err := AccessTokenTypeString(str) + if err != nil { + return err + } + + *i = val + return nil +} + +// MarshalGQL implements the graphql.Marshaler interface for AccessTokenType +func (i AccessTokenType) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(i.String())) +} + +// UnmarshalGQL implements the graphql.Unmarshaler interface for AccessTokenType +func (i *AccessTokenType) UnmarshalGQL(value interface{}) error { + str, ok := value.(string) + if !ok { + return fmt.Errorf("AccessTokenType should be a string, got %T", value) + } + + var err error + *i, err = AccessTokenTypeString(str) + return err +} diff --git a/pkg/op/client.go b/pkg/op/client.go index d3d32b7..d9f7ab0 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -6,15 +6,18 @@ import ( "github.com/zitadel/oidc/pkg/oidc" ) +//go:generate go get github.com/dmarkham/enumer +//go:generate go run github.com/dmarkham/enumer -linecomment -sql -json -text -yaml -gqlgen -type=ApplicationType,AccessTokenType + const ( - ApplicationTypeWeb ApplicationType = iota - ApplicationTypeUserAgent - ApplicationTypeNative + ApplicationTypeWeb ApplicationType = iota // web + ApplicationTypeUserAgent // user_agent + ApplicationTypeNative // native ) const ( - AccessTokenTypeBearer AccessTokenType = iota - AccessTokenTypeJWT + AccessTokenTypeBearer AccessTokenType = iota // bearer + AccessTokenTypeJWT // JWT ) type ApplicationType int From 53ede2ee8c327e6a99cbff069c7f8eede1ddaeed Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Wed, 27 Jul 2022 08:36:43 +0200 Subject: [PATCH 120/502] fix: use default redirect uri when not passed on end_session endpoint (#201) --- pkg/oidc/session.go | 1 + pkg/op/session.go | 67 ++++++++++++++++++++++++++++----------------- pkg/op/storage.go | 2 +- 3 files changed, 44 insertions(+), 26 deletions(-) diff --git a/pkg/oidc/session.go b/pkg/oidc/session.go index d6735b4..9aa70c1 100644 --- a/pkg/oidc/session.go +++ b/pkg/oidc/session.go @@ -4,6 +4,7 @@ package oidc //https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout type EndSessionRequest struct { IdTokenHint string `schema:"id_token_hint"` + ClientID string `schema:"client_id"` PostLogoutRedirectURI string `schema:"post_logout_redirect_uri"` State string `schema:"state"` } diff --git a/pkg/op/session.go b/pkg/op/session.go index a1c2d90..c4984fc 100644 --- a/pkg/op/session.go +++ b/pkg/op/session.go @@ -3,6 +3,7 @@ package op import ( "context" "net/http" + "net/url" httphelper "github.com/zitadel/oidc/pkg/http" "github.com/zitadel/oidc/pkg/oidc" @@ -32,11 +33,7 @@ func EndSession(w http.ResponseWriter, r *http.Request, ender SessionEnder) { RequestError(w, r, err) return } - var clientID string - if session.Client != nil { - clientID = session.Client.GetID() - } - err = ender.Storage().TerminateSession(r.Context(), session.UserID, clientID) + err = ender.Storage().TerminateSession(r.Context(), session.UserID, session.ClientID) if err != nil { RequestError(w, r, oidc.DefaultToServerError(err, "error terminating session")) return @@ -58,28 +55,48 @@ func ParseEndSessionRequest(r *http.Request, decoder httphelper.Decoder) (*oidc. } func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest, ender SessionEnder) (*EndSessionRequest, error) { - session := new(EndSessionRequest) - if req.IdTokenHint == "" { - return session, nil + session := &EndSessionRequest{ + RedirectURI: ender.DefaultLogoutRedirectURI(), } - claims, err := VerifyIDTokenHint(ctx, req.IdTokenHint, ender.IDTokenHintVerifier()) - if err != nil { - return nil, oidc.ErrInvalidRequest().WithDescription("id_token_hint invalid").WithParent(err) + if req.IdTokenHint != "" { + claims, err := VerifyIDTokenHint(ctx, req.IdTokenHint, ender.IDTokenHintVerifier()) + if err != nil { + return nil, oidc.ErrInvalidRequest().WithDescription("id_token_hint invalid").WithParent(err) + } + session.UserID = claims.GetSubject() + if req.ClientID != "" && req.ClientID != claims.GetAuthorizedParty() { + return nil, oidc.ErrInvalidRequest().WithDescription("client_id does not match azp of id_token_hint") + } + req.ClientID = claims.GetAuthorizedParty() } - session.UserID = claims.GetSubject() - session.Client, err = ender.Storage().GetClientByClientID(ctx, claims.GetAuthorizedParty()) - if err != nil { - return nil, oidc.DefaultToServerError(err, "") - } - if req.PostLogoutRedirectURI == "" { - session.RedirectURI = ender.DefaultLogoutRedirectURI() - return session, nil - } - for _, uri := range session.Client.PostLogoutRedirectURIs() { - if uri == req.PostLogoutRedirectURI { - session.RedirectURI = uri + "?state=" + req.State - return session, nil + if req.ClientID != "" { + client, err := ender.Storage().GetClientByClientID(ctx, req.ClientID) + if err != nil { + return nil, oidc.DefaultToServerError(err, "") + } + session.ClientID = client.GetID() + if req.PostLogoutRedirectURI != "" { + if err := ValidateEndSessionPostLogoutRedirectURI(req.PostLogoutRedirectURI, client); err != nil { + return nil, err + } + session.RedirectURI = req.PostLogoutRedirectURI } } - return nil, oidc.ErrInvalidRequest().WithDescription("post_logout_redirect_uri invalid") + if req.State != "" { + redirect, err := url.Parse(session.RedirectURI) + if err != nil { + return nil, oidc.DefaultToServerError(err, "") + } + session.RedirectURI = mergeQueryParams(redirect, url.Values{"state": {req.State}}) + } + return session, nil +} + +func ValidateEndSessionPostLogoutRedirectURI(postLogoutRedirectURI string, client Client) error { + for _, uri := range client.PostLogoutRedirectURIs() { + if uri == postLogoutRedirectURI { + return nil + } + } + return oidc.ErrInvalidRequest().WithDescription("post_logout_redirect_uri invalid") } diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 1b90a98..0cdffce 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -54,6 +54,6 @@ type StorageNotFoundError interface { type EndSessionRequest struct { UserID string - Client Client + ClientID string RedirectURI string } From 0b4d62c7458e31ec95c2f1bc96386b54866e33bf Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Fri, 5 Aug 2022 01:54:40 -0700 Subject: [PATCH 121/502] chore: add comments documenting Storage and AuthStorage (#193) * add comments documenting Storage and AuthStorage * JWTTokenRequest is a pointer * note that token strings are actually tokenIDs * review feedback * remove suggestion that CreateAccessToken could be called with retrun from AuthStorage.TokenRequestByRefreshToken --- pkg/op/storage.go | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 0cdffce..32905f1 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -16,12 +16,30 @@ type AuthStorage interface { SaveAuthCode(context.Context, string, string) error DeleteAuthRequest(context.Context, string) error - CreateAccessToken(context.Context, TokenRequest) (string, time.Time, error) - CreateAccessAndRefreshTokens(ctx context.Context, request TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) - TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (RefreshTokenRequest, error) + // The TokenRequest parameter of CreateAccessToken can be any of: + // + // * TokenRequest as returned by ClientCredentialsStorage.ClientCredentialsTokenRequest, + // + // * AuthRequest as returned by AuthRequestByID or AuthRequestByCode (above) + // + // * *oidc.JWTTokenRequest from a JWT that is the assertion value of a JWT Profile + // Grant: https://datatracker.ietf.org/doc/html/rfc7523#section-2.1 + CreateAccessToken(context.Context, TokenRequest) (accessTokenID string, expiration time.Time, err error) + + // The TokenRequest parameter of CreateAccessAndRefreshTokens can be any of: + // + // * TokenRequest as returned by ClientCredentialsStorage.ClientCredentialsTokenRequest + // + // * RefreshTokenRequest as returned by AuthStorage.TokenRequestByRefreshToken + // + // * AuthRequest as by returned by the AuthRequestByID or AuthRequestByCode (above). + // Used for the authorization code flow which requested offline_access scope and + // registered the refresh_token grant type in advance + CreateAccessAndRefreshTokens(ctx context.Context, request TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshTokenID string, expiration time.Time, err error) + TokenRequestByRefreshToken(ctx context.Context, refreshTokenID string) (RefreshTokenRequest, error) TerminateSession(ctx context.Context, userID string, clientID string) error - RevokeToken(ctx context.Context, token string, userID string, clientID string) *oidc.Error + RevokeToken(ctx context.Context, tokenID string, userID string, clientID string) *oidc.Error GetSigningKey(context.Context, chan<- jose.SigningKey) GetKeySet(context.Context) (*jose.JSONWebKeySet, error) @@ -42,6 +60,11 @@ type OPStorage interface { ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error) } +// Storage is a required parameter for NewOpenIDProvider(). In addition to the +// embedded interfaces below, if the passed Storage implements ClientCredentialsStorage +// then the grant type "client_credentials" will be supported. In that case, the access +// token returned by CreateAccessToken should be a JWT. +// See https://datatracker.ietf.org/doc/html/rfc6749#section-1.3.4 for context. type Storage interface { AuthStorage OPStorage From 94871afbcb5f37e263d18f0e73b75bd221e6aca7 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Fri, 5 Aug 2022 01:57:50 -0700 Subject: [PATCH 122/502] feat: add rp.RefreshAccessToken (#198) * chore: make tokenEndpointCaller public * add RelyingParty function * undo changes made by gofumpt * undo more gofumpt changes * undo more gofumpt changes --- pkg/client/client.go | 6 +++--- pkg/client/jwt_profile.go | 4 ++-- pkg/client/rp/relying_party.go | 20 ++++++++++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index 58986bb..ccc3cc0 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -48,16 +48,16 @@ func Discover(issuer string, httpClient *http.Client, wellKnownUrl ...string) (* return discoveryConfig, nil } -type tokenEndpointCaller interface { +type TokenEndpointCaller interface { TokenEndpoint() string HttpClient() *http.Client } -func CallTokenEndpoint(request interface{}, caller tokenEndpointCaller) (newToken *oauth2.Token, err error) { +func CallTokenEndpoint(request interface{}, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) { return callTokenEndpoint(request, nil, caller) } -func callTokenEndpoint(request interface{}, authFn interface{}, caller tokenEndpointCaller) (newToken *oauth2.Token, err error) { +func callTokenEndpoint(request interface{}, authFn interface{}, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) { req, err := httphelper.FormRequest(caller.TokenEndpoint(), request, Encoder, authFn) if err != nil { return nil, err diff --git a/pkg/client/jwt_profile.go b/pkg/client/jwt_profile.go index 8bb6f4b..a711de9 100644 --- a/pkg/client/jwt_profile.go +++ b/pkg/client/jwt_profile.go @@ -9,8 +9,8 @@ import ( "github.com/zitadel/oidc/pkg/oidc" ) -//JWTProfileExchange handles the oauth2 jwt profile exchange -func JWTProfileExchange(jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, caller tokenEndpointCaller) (*oauth2.Token, error) { +// JWTProfileExchange handles the oauth2 jwt profile exchange +func JWTProfileExchange(jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, caller TokenEndpointCaller) (*oauth2.Token, error) { return CallTokenEndpoint(jwtProfileGrantRequest, caller) } diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 5fbce27..3094f23 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -536,3 +536,23 @@ func WithClientAssertionJWT(clientAssertion string) CodeExchangeOpt { return client.ClientAssertionCodeOptions(clientAssertion) } } + +type tokenEndpointCaller struct { + RelyingParty +} + +func (t tokenEndpointCaller) TokenEndpoint() string { + return t.OAuthConfig().Endpoint.TokenURL +} + +func RefreshAccessToken(rp RelyingParty, refreshToken, clientAssertion, clientAssertionType string) (*oauth2.Token, error) { + request := oidc.RefreshTokenRequest{ + RefreshToken: refreshToken, + Scopes: rp.OAuthConfig().Scopes, + ClientID: rp.OAuthConfig().ClientID, + ClientSecret: rp.OAuthConfig().ClientSecret, + ClientAssertion: clientAssertion, + ClientAssertionType: clientAssertionType, + } + return client.CallTokenEndpoint(request, tokenEndpointCaller{RelyingParty: rp}) +} From 0e7949b1a08f992018e9d3da2469d70a4b56d48c Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Mon, 8 Aug 2022 15:02:36 +0200 Subject: [PATCH 123/502] chore: add go 1.19 to matrix build (#202) * chore: add go 1.19 to matrix build * try rc2 * use rc * remove rc and update readme * update ubuntu version --- .github/workflows/release.yml | 6 +++--- README.md | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2496a7c..d95a3c6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,10 +12,10 @@ on: jobs: test: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 strategy: matrix: - go: ['1.15', '1.16', '1.17', '1.18'] + go: ['1.15', '1.16', '1.17', '1.18', '1.19'] name: Go ${{ matrix.go }} test steps: - uses: actions/checkout@v3 @@ -29,7 +29,7 @@ jobs: file: ./profile.cov name: codecov-go release: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 needs: [test] if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' }} env: diff --git a/README.md b/README.md index 872b9b6..4d32cc3 100644 --- a/README.md +++ b/README.md @@ -90,8 +90,9 @@ Versions that also build are marked with :warning:. | <1.15 | :x: | | 1.15 | :warning: | | 1.16 | :warning: | -| 1.17 | :white_check_mark: | +| 1.17 | :warning: | | 1.18 | :white_check_mark: | +| 1.19 | :white_check_mark: | ## Why another library From fca6cf94339eacb7db9b4f5fc5c9b8e66781c495 Mon Sep 17 00:00:00 2001 From: Igor Morozov Date: Tue, 30 Aug 2022 17:09:56 +0300 Subject: [PATCH 124/502] feat: get all claims (#209) --- pkg/oidc/introspection.go | 4 ++++ pkg/oidc/userinfo.go | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index 33ba2cb..3ff7c66 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -140,6 +140,10 @@ func (i *introspectionResponse) GetClaim(key string) interface{} { return i.claims[key] } +func (i *introspectionResponse) GetClaims() map[string]interface{} { + return i.claims +} + func (i *introspectionResponse) SetActive(active bool) { i.Active = active } diff --git a/pkg/oidc/userinfo.go b/pkg/oidc/userinfo.go index afc2ad0..4d524e3 100644 --- a/pkg/oidc/userinfo.go +++ b/pkg/oidc/userinfo.go @@ -15,6 +15,7 @@ type UserInfo interface { UserInfoPhone GetAddress() UserInfoAddress GetClaim(key string) interface{} + GetClaims() map[string]interface{} } type UserInfoProfile interface { @@ -173,6 +174,10 @@ func (u *userinfo) GetClaim(key string) interface{} { return u.claims[key] } +func (u *userinfo) GetClaims() map[string]interface{} { + return u.claims +} + func (u *userinfo) SetSubject(sub string) { u.Subject = sub } From 0719efa51a7978974f1511c0a647b680700d8215 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Sep 2022 08:12:02 +0200 Subject: [PATCH 125/502] chore(deps): bump codecov/codecov-action from 3.1.0 to 3.1.1 (#212) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3.1.0 to 3.1.1. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v3.1.0...v3.1.1) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d95a3c6..8912cd4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov ./pkg/... - - uses: codecov/codecov-action@v3.1.0 + - uses: codecov/codecov-action@v3.1.1 with: file: ./profile.cov name: codecov-go From 98851d4ca6804914de0dc1aab82fc2c77b686611 Mon Sep 17 00:00:00 2001 From: Fabi <38692350+hifabienne@users.noreply.github.com> Date: Tue, 27 Sep 2022 08:12:54 +0200 Subject: [PATCH 126/502] chore(workflows): add issues to project board (#213) * Create main.yml * Rename main.yml to issue.yml --- .github/workflows/issue.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/issue.yml diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml new file mode 100644 index 0000000..61f49a4 --- /dev/null +++ b/.github/workflows/issue.yml @@ -0,0 +1,18 @@ +name: Add new issues to kanban project + +on: + issues: + types: + - opened + +jobs: + add-to-project: + name: Add issue to project + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@v0.3.0 + with: + # You can target a repository in a different organization + # to the issue + project-url: https://github.com/orgs/zitadel/projects/1 + github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} From 0d721d937e7d60d2284647ad3d33f0711271079f Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Thu, 29 Sep 2022 22:18:08 -0700 Subject: [PATCH 127/502] chore: adjustments to comments for things found while implementing Storage --- pkg/client/rp/relying_party.go | 2 ++ pkg/oidc/token_request.go | 2 ++ pkg/op/op.go | 3 ++- pkg/op/storage.go | 5 ++++- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 3094f23..9245c8c 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -248,6 +248,8 @@ func WithClientKey(path string) Option { } // WithJWTProfile creates a signer used for the JWT Profile Client Authentication on the token endpoint +// When creating the signer, be sure to include the KeyID in the SigningKey. +// See client.NewSignerFromPrivateKeyByte for an example. func WithJWTProfile(signerFromKey SignerFromKey) Option { return func(rp *relyingParty) error { signer, err := signerFromKey() diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index 989e792..a25da9c 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -70,6 +70,8 @@ func (a *AccessTokenRequest) SetClientSecret(clientSecret string) { a.ClientSecret = clientSecret } +// RefreshTokenRequest is not useful for making refresh requests because the +// grant_type is not included explicitly but rather implied. type RefreshTokenRequest struct { RefreshToken string `schema:"refresh_token"` Scopes SpaceDelimitedArray `schema:"scope"` diff --git a/pkg/op/op.go b/pkg/op/op.go index 5b8567a..69d6d39 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -133,7 +133,8 @@ type endpoints struct { //This does not include login. Login is handled with a redirect that includes the //request ID. The redirect for logins is specified per-client by Client.LoginURL(). //Successful logins should mark the request as authorized and redirect back to to -//op.AuthCallbackURL(provider) which is probably /callback. +//op.AuthCallbackURL(provider) which is probably /callback. On the redirect back +// to the AuthCallbackURL, the request id should be passed as the "id" parameter. func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opOpts ...Option) (OpenIDProvider, error) { err := ValidateIssuer(config.Issuer) if err != nil { diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 32905f1..da536b9 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -56,7 +56,10 @@ type OPStorage interface { SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, clientID string) error GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) - GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) + + // GetKeyByIDAndUserID is mis-named. It does not pass userID. Instead + // it passes the clientID. + GetKeyByIDAndUserID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error) ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error) } From c0badf2329854ccf06d5a61b4430c381e38da7b8 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Thu, 29 Sep 2022 22:18:48 -0700 Subject: [PATCH 128/502] chore: additional errors and error improvements that catch problems earlier --- pkg/op/auth_request.go | 13 +++++++++++++ pkg/op/token_code.go | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 2ebedb5..35e6521 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -2,6 +2,7 @@ package op import ( "context" + "fmt" "net" "net/http" "net/url" @@ -78,6 +79,14 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { return } } + if authReq.ClientID == "" { + AuthRequestError(w, r, authReq, fmt.Errorf("auth request is missing client_id"), authorizer.Encoder()) + return + } + if authReq.RedirectURI == "" { + AuthRequestError(w, r, authReq, fmt.Errorf("auth request is missing redirect_uri"), authorizer.Encoder()) + return + } validation := ValidateAuthRequest if validater, ok := authorizer.(AuthorizeValidator); ok { validation = validater.ValidateAuthRequest @@ -378,6 +387,10 @@ func RedirectToLogin(authReqID string, client Client, w http.ResponseWriter, r * func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { params := mux.Vars(r) id := params["id"] + if id == "" { + AuthRequestError(w, r, nil, fmt.Errorf("auth request callback is missing id"), authorizer.Encoder()) + return + } authReq, err := authorizer.Storage().AuthRequestByID(r.Context(), id) if err != nil { diff --git a/pkg/op/token_code.go b/pkg/op/token_code.go index b21871e..185fad8 100644 --- a/pkg/op/token_code.go +++ b/pkg/op/token_code.go @@ -53,7 +53,7 @@ func ValidateAccessTokenRequest(ctx context.Context, tokenReq *oidc.AccessTokenR return nil, nil, oidc.ErrInvalidGrant() } if !ValidateGrantType(client, oidc.GrantTypeCode) { - return nil, nil, oidc.ErrUnauthorizedClient() + return nil, nil, oidc.ErrUnauthorizedClient().WithDescription("client missing grant type " + string(oidc.GrantTypeCode)) } if tokenReq.RedirectURI != authReq.GetRedirectURI() { return nil, nil, oidc.ErrInvalidGrant().WithDescription("redirect_uri does not correspond") From 4b4b0e49e0ea49add4f5692345c82fb3f9c6673b Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Thu, 29 Sep 2022 22:24:47 -0700 Subject: [PATCH 129/502] chore: update jwtProfileKeySet to match actual use (#219) --- pkg/op/auth_request.go | 2 +- pkg/op/verifier_jwt_profile.go | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 35e6521..979fe62 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -148,7 +148,7 @@ func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage if !str.Contains(requestObject.Audience, issuer) { return authReq, oidc.ErrInvalidRequest() } - keySet := &jwtProfileKeySet{storage, requestObject.Issuer} + keySet := &jwtProfileKeySet{storage: storage, clientID: requestObject.Issuer} if err = oidc.CheckSignature(ctx, authReq.RequestParam, payload, requestObject, nil, keySet); err != nil { return authReq, err } diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index efaec95..90dfc11 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -96,8 +96,7 @@ func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerif return nil, err } - keySet := &jwtProfileKeySet{v.Storage(), request.Issuer} - + keySet := &jwtProfileKeySet{storage: v.Storage(), clientID: request.Issuer} if err = oidc.CheckSignature(ctx, assertion, payload, request, nil, keySet); err != nil { return nil, err } @@ -116,14 +115,14 @@ func SubjectIsIssuer(request *oidc.JWTTokenRequest) error { } type jwtProfileKeySet struct { - storage jwtProfileKeyStorage - userID string + storage jwtProfileKeyStorage + clientID string } //VerifySignature implements oidc.KeySet by getting the public key from Storage implementation func (k *jwtProfileKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) { keyID, _ := oidc.GetKeyIDAndAlg(jws) - key, err := k.storage.GetKeyByIDAndUserID(ctx, keyID, k.userID) + key, err := k.storage.GetKeyByIDAndUserID(ctx, keyID, k.clientID) if err != nil { return nil, fmt.Errorf("error fetching keys: %w", err) } From 88a98c03ea22995619eb7e57ac9f58ea4fe16791 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Thu, 29 Sep 2022 22:28:31 -0700 Subject: [PATCH 130/502] fix: rp.RefreshAccessToken did not work (#216) * oidc.RefreshTokenRequest cannot be used to in a request to refresh tokens because it does not explicitly include grant_types. * fix merge issue * undo accidental formatting changes --- pkg/client/rp/relying_party.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 9245c8c..af202a3 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -547,14 +547,25 @@ func (t tokenEndpointCaller) TokenEndpoint() string { return t.OAuthConfig().Endpoint.TokenURL } +type RefreshTokenRequest struct { + RefreshToken string `schema:"refresh_token"` + Scopes oidc.SpaceDelimitedArray `schema:"scope"` + ClientID string `schema:"client_id"` + ClientSecret string `schema:"client_secret"` + ClientAssertion string `schema:"client_assertion"` + ClientAssertionType string `schema:"client_assertion_type"` + GrantType oidc.GrantType `schema:"grant_type"` +} + func RefreshAccessToken(rp RelyingParty, refreshToken, clientAssertion, clientAssertionType string) (*oauth2.Token, error) { - request := oidc.RefreshTokenRequest{ + request := RefreshTokenRequest{ RefreshToken: refreshToken, Scopes: rp.OAuthConfig().Scopes, ClientID: rp.OAuthConfig().ClientID, ClientSecret: rp.OAuthConfig().ClientSecret, ClientAssertion: clientAssertion, ClientAssertionType: clientAssertionType, + GrantType: oidc.GrantTypeRefreshToken, } return client.CallTokenEndpoint(request, tokenEndpointCaller{RelyingParty: rp}) } From 29904e9446031e3edb1dba38df94dc5ef8e8843a Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Fri, 30 Sep 2022 07:28:54 +0200 Subject: [PATCH 131/502] chore: add notice file to explicit state the copyright (#215) --- NOTICE | 1 + 1 file changed, 1 insertion(+) create mode 100644 NOTICE diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..a5f5f7a --- /dev/null +++ b/NOTICE @@ -0,0 +1 @@ +Copyright The zitadel/oidc Contributors From 2d248b1a1a9a3a1299af8d55462def7498d29447 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Thu, 29 Sep 2022 22:39:23 -0700 Subject: [PATCH 132/502] fix: Change op.tokenHandler to follow the same pattern as the rest of the endpoint handlers (#210) inside op: provide a standard endpoint handler that uses injected data. --- pkg/op/token_request.go | 61 ++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/pkg/op/token_request.go b/pkg/op/token_request.go index 71bf077..dc8d118 100644 --- a/pkg/op/token_request.go +++ b/pkg/op/token_request.go @@ -25,37 +25,42 @@ type Exchanger interface { func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - grantType := r.FormValue("grant_type") - switch grantType { - case string(oidc.GrantTypeCode): - CodeExchange(w, r, exchanger) - return - case string(oidc.GrantTypeRefreshToken): - if exchanger.GrantTypeRefreshTokenSupported() { - RefreshTokenExchange(w, r, exchanger) - return - } - case string(oidc.GrantTypeBearer): - if ex, ok := exchanger.(JWTAuthorizationGrantExchanger); ok && exchanger.GrantTypeJWTAuthorizationSupported() { - JWTProfile(w, r, ex) - return - } - case string(oidc.GrantTypeTokenExchange): - if exchanger.GrantTypeTokenExchangeSupported() { - TokenExchange(w, r, exchanger) - return - } - case string(oidc.GrantTypeClientCredentials): - if exchanger.GrantTypeClientCredentialsSupported() { - ClientCredentialsExchange(w, r, exchanger) - return - } - case "": - RequestError(w, r, oidc.ErrInvalidRequest().WithDescription("grant_type missing")) + Exchange(w, r, exchanger) + } +} + +//Exchange performs a token exchange appropriate for the grant type +func Exchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { + grantType := r.FormValue("grant_type") + switch grantType { + case string(oidc.GrantTypeCode): + CodeExchange(w, r, exchanger) + return + case string(oidc.GrantTypeRefreshToken): + if exchanger.GrantTypeRefreshTokenSupported() { + RefreshTokenExchange(w, r, exchanger) return } - RequestError(w, r, oidc.ErrUnsupportedGrantType().WithDescription("%s not supported", grantType)) + case string(oidc.GrantTypeBearer): + if ex, ok := exchanger.(JWTAuthorizationGrantExchanger); ok && exchanger.GrantTypeJWTAuthorizationSupported() { + JWTProfile(w, r, ex) + return + } + case string(oidc.GrantTypeTokenExchange): + if exchanger.GrantTypeTokenExchangeSupported() { + TokenExchange(w, r, exchanger) + return + } + case string(oidc.GrantTypeClientCredentials): + if exchanger.GrantTypeClientCredentialsSupported() { + ClientCredentialsExchange(w, r, exchanger) + return + } + case "": + RequestError(w, r, oidc.ErrInvalidRequest().WithDescription("grant_type missing")) + return } + RequestError(w, r, oidc.ErrUnsupportedGrantType().WithDescription("%s not supported", grantType)) } //AuthenticatedTokenRequest is a helper interface for ParseAuthenticatedTokenRequest From 328d0e12517cd101bcf103feb146e92cc0720197 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Thu, 29 Sep 2022 22:39:40 -0700 Subject: [PATCH 133/502] feat: add access token verifier ops to openidProvider (#221) --- pkg/op/op.go | 36 ++++++++++++++++++++------------- pkg/op/verifier_access_token.go | 13 +++++++++++- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/pkg/op/op.go b/pkg/op/op.go index 69d6d39..bea8569 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -171,20 +171,21 @@ func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opO } type openidProvider struct { - config *Config - endpoints *endpoints - storage Storage - signer Signer - idTokenHintVerifier IDTokenHintVerifier - jwtProfileVerifier JWTProfileVerifier - accessTokenVerifier AccessTokenVerifier - keySet *openIDKeySet - crypto Crypto - httpHandler http.Handler - decoder *schema.Decoder - encoder *schema.Encoder - interceptors []HttpInterceptor - timer <-chan time.Time + config *Config + endpoints *endpoints + storage Storage + signer Signer + idTokenHintVerifier IDTokenHintVerifier + jwtProfileVerifier JWTProfileVerifier + accessTokenVerifier AccessTokenVerifier + keySet *openIDKeySet + crypto Crypto + httpHandler http.Handler + decoder *schema.Decoder + encoder *schema.Encoder + interceptors []HttpInterceptor + timer <-chan time.Time + accessTokenVerifierOpts []AccessTokenVerifierOpt } func (o *openidProvider) Issuer() string { @@ -453,6 +454,13 @@ func WithHttpInterceptors(interceptors ...HttpInterceptor) Option { } } +func WithAccessTokenVerifierOpts(opts ...AccessTokenVerifierOpt) Option { + return func(o *openidProvider) error { + o.accessTokenVerifierOpts = opts + return nil + } +} + func buildInterceptor(interceptors ...HttpInterceptor) func(http.HandlerFunc) http.Handler { return func(handlerFunc http.HandlerFunc) http.Handler { handler := handlerFuncToHandler(handlerFunc) diff --git a/pkg/op/verifier_access_token.go b/pkg/op/verifier_access_token.go index 2f19e91..d2c0c80 100644 --- a/pkg/op/verifier_access_token.go +++ b/pkg/op/verifier_access_token.go @@ -48,11 +48,22 @@ func (i *accessTokenVerifier) KeySet() oidc.KeySet { return i.keySet } -func NewAccessTokenVerifier(issuer string, keySet oidc.KeySet) AccessTokenVerifier { +type AccessTokenVerifierOpt func(*accessTokenVerifier) + +func WithSupportedAccessTokenSigningAlgorithms(algs ...string) AccessTokenVerifierOpt { + return func(verifier *accessTokenVerifier) { + verifier.supportedSignAlgs = algs + } +} + +func NewAccessTokenVerifier(issuer string, keySet oidc.KeySet, opts ...AccessTokenVerifierOpt) AccessTokenVerifier { verifier := &accessTokenVerifier{ issuer: issuer, keySet: keySet, } + for _, opt := range opts { + opt(verifier) + } return verifier } From 62daf4cc42545f5a1ad6c9216f5ea5a86d1304ed Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Thu, 29 Sep 2022 22:40:05 -0700 Subject: [PATCH 134/502] feat: add WithPath CookieHandlerOpt (#217) --- pkg/http/cookie.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/http/cookie.go b/pkg/http/cookie.go index 62ea295..4949b77 100644 --- a/pkg/http/cookie.go +++ b/pkg/http/cookie.go @@ -13,6 +13,7 @@ type CookieHandler struct { sameSite http.SameSite maxAge int domain string + path string } func NewCookieHandler(hashKey, encryptKey []byte, opts ...CookieHandlerOpt) *CookieHandler { @@ -20,6 +21,7 @@ func NewCookieHandler(hashKey, encryptKey []byte, opts ...CookieHandlerOpt) *Coo securecookie: securecookie.New(hashKey, encryptKey), secureOnly: true, sameSite: http.SameSiteLaxMode, + path: "/", } for _, opt := range opts { @@ -55,6 +57,12 @@ func WithDomain(domain string) CookieHandlerOpt { } } +func WithPath(path string) CookieHandlerOpt { + return func(c *CookieHandler) { + c.domain = path + } +} + func (c *CookieHandler) CheckCookie(r *http.Request, name string) (string, error) { cookie, err := r.Cookie(name) if err != nil { @@ -87,7 +95,7 @@ func (c *CookieHandler) SetCookie(w http.ResponseWriter, name, value string) err Name: name, Value: encoded, Domain: c.domain, - Path: "/", + Path: c.path, MaxAge: c.maxAge, HttpOnly: true, Secure: c.secureOnly, @@ -101,7 +109,7 @@ func (c *CookieHandler) DeleteCookie(w http.ResponseWriter, name string) { Name: name, Value: "", Domain: c.domain, - Path: "/", + Path: c.path, MaxAge: -1, HttpOnly: true, Secure: c.secureOnly, From 749c30491b937e0da73a69b691723b3fbb56d9d8 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Thu, 29 Sep 2022 22:44:10 -0700 Subject: [PATCH 135/502] chore: Make example/server usable for tests (#205) * internal -> storage; split users into an interface * move example/server/*.go to example/server/exampleop/ * export all User fields * storage -> Storage * example server now passes tests --- example/server/{ => exampleop}/login.go | 10 +- example/server/exampleop/op.go | 116 ++++ example/server/internal/storage.go | 553 ---------------- example/server/internal/user.go | 24 - example/server/main.go | 32 + example/server/op.go | 126 ---- .../server/{internal => storage}/client.go | 71 ++- example/server/{internal => storage}/oidc.go | 18 +- example/server/storage/storage.go | 590 ++++++++++++++++++ example/server/{internal => storage}/token.go | 2 +- example/server/storage/user.go | 71 +++ 11 files changed, 860 insertions(+), 753 deletions(-) rename example/server/{ => exampleop}/login.go (91%) create mode 100644 example/server/exampleop/op.go delete mode 100644 example/server/internal/storage.go delete mode 100644 example/server/internal/user.go create mode 100644 example/server/main.go delete mode 100644 example/server/op.go rename example/server/{internal => storage}/client.go (60%) rename example/server/{internal => storage}/oidc.go (87%) create mode 100644 example/server/storage/storage.go rename example/server/{internal => storage}/token.go (96%) create mode 100644 example/server/storage/user.go diff --git a/example/server/login.go b/example/server/exampleop/login.go similarity index 91% rename from example/server/login.go rename to example/server/exampleop/login.go index 90d01d8..fd3dead 100644 --- a/example/server/login.go +++ b/example/server/exampleop/login.go @@ -1,4 +1,4 @@ -package main +package exampleop import ( "fmt" @@ -12,8 +12,7 @@ const ( queryAuthRequestID = "authRequestID" ) -var ( - loginTmpl, _ = template.New("login").Parse(` +var loginTmpl, _ = template.New("login").Parse(` @@ -41,7 +40,6 @@ var ( `) -) type login struct { authenticate authenticate @@ -74,8 +72,8 @@ func (l *login) loginHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprintf("cannot parse form:%s", err), http.StatusInternalServerError) return } - //the oidc package will pass the id of the auth request as query parameter - //we will use this id through the login process and therefore pass it to the login page + // the oidc package will pass the id of the auth request as query parameter + // we will use this id through the login process and therefore pass it to the login page renderLogin(w, r.FormValue(queryAuthRequestID), nil) } diff --git a/example/server/exampleop/op.go b/example/server/exampleop/op.go new file mode 100644 index 0000000..4794d8a --- /dev/null +++ b/example/server/exampleop/op.go @@ -0,0 +1,116 @@ +package exampleop + +import ( + "context" + "crypto/sha256" + "log" + "net/http" + "os" + + "github.com/gorilla/mux" + "golang.org/x/text/language" + + "github.com/zitadel/oidc/example/server/storage" + "github.com/zitadel/oidc/pkg/op" +) + +const ( + pathLoggedOut = "/logged-out" +) + +func init() { + storage.RegisterClients( + storage.NativeClient("native"), + storage.WebClient("web", "secret"), + storage.WebClient("api", "secret"), + ) +} + +type Storage interface { + op.Storage + CheckUsernamePassword(username, password, id string) error +} + +// SetupServer creates an OIDC server with Issuer=http://localhost: +// +// Use one of the pre-made clients in storage/clients.go or register a new one. +func SetupServer(ctx context.Context, issuer string, storage Storage) *mux.Router { + // this will allow us to use an issuer with http:// instead of https:// + os.Setenv(op.OidcDevMode, "true") + + // the OpenID Provider requires a 32-byte key for (token) encryption + // be sure to create a proper crypto random key and manage it securely! + key := sha256.Sum256([]byte("test")) + + router := mux.NewRouter() + + // for simplicity, we provide a very small default page for users who have signed out + router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) { + _, err := w.Write([]byte("signed out successfully")) + if err != nil { + log.Printf("error serving logged out page: %v", err) + } + }) + + // creation of the OpenIDProvider with the just created in-memory Storage + provider, err := newOP(ctx, storage, issuer, key) + if err != nil { + log.Fatal(err) + } + + // the provider will only take care of the OpenID Protocol, so there must be some sort of UI for the login process + // for the simplicity of the example this means a simple page with username and password field + l := NewLogin(storage, op.AuthCallbackURL(provider)) + + // regardless of how many pages / steps there are in the process, the UI must be registered in the router, + // so we will direct all calls to /login to the login UI + router.PathPrefix("/login/").Handler(http.StripPrefix("/login", l.router)) + + // we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration) + // is served on the correct path + // + // if your issuer ends with a path (e.g. http://localhost:9998/custom/path/), + // then you would have to set the path prefix (/custom/path/) + router.PathPrefix("/").Handler(provider.HttpHandler()) + + return router +} + +// newOP will create an OpenID Provider for localhost on a specified port with a given encryption key +// and a predefined default logout uri +// it will enable all options (see descriptions) +func newOP(ctx context.Context, storage op.Storage, issuer string, key [32]byte) (op.OpenIDProvider, error) { + config := &op.Config{ + Issuer: issuer, + CryptoKey: key, + + // will be used if the end_session endpoint is called without a post_logout_redirect_uri + DefaultLogoutRedirectURI: pathLoggedOut, + + // enables code_challenge_method S256 for PKCE (and therefore PKCE in general) + CodeMethodS256: true, + + // enables additional client_id/client_secret authentication by form post (not only HTTP Basic Auth) + AuthMethodPost: true, + + // enables additional authentication by using private_key_jwt + AuthMethodPrivateKeyJWT: true, + + // enables refresh_token grant use + GrantTypeRefreshToken: true, + + // enables use of the `request` Object parameter + RequestObjectSupported: true, + + // this example has only static texts (in English), so we'll set the here accordingly + SupportedUILocales: []language.Tag{language.English}, + } + handler, err := op.NewOpenIDProvider(ctx, config, storage, + // as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth + op.WithCustomAuthEndpoint(op.NewEndpoint("auth")), + ) + if err != nil { + return nil, err + } + return handler, nil +} diff --git a/example/server/internal/storage.go b/example/server/internal/storage.go deleted file mode 100644 index 5fd61c5..0000000 --- a/example/server/internal/storage.go +++ /dev/null @@ -1,553 +0,0 @@ -package internal - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "fmt" - "math/big" - "time" - - "github.com/google/uuid" - "golang.org/x/text/language" - "gopkg.in/square/go-jose.v2" - - "github.com/zitadel/oidc/pkg/oidc" - "github.com/zitadel/oidc/pkg/op" -) - -var ( - //serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant - //the corresponding private key is in the service-key1.json (for demonstration purposes) - serviceKey1 = &rsa.PublicKey{ - N: func() *big.Int { - n, _ := new(big.Int).SetString("00f6d44fb5f34ac2033a75e73cb65ff24e6181edc58845e75a560ac21378284977bb055b1a75b714874e2a2641806205681c09abec76efd52cf40984edcf4c8ca09717355d11ac338f280d3e4c905b00543bdb8ee5a417496cb50cb0e29afc5a0d0471fd5a2fa625bd5281f61e6b02067d4fe7a5349eeae6d6a4300bcd86eef331", 16) - return n - }(), - E: 65537, - } -) - -//storage implements the op.Storage interface -//typically you would implement this as a layer on top of your database -//for simplicity this example keeps everything in-memory -type storage struct { - authRequests map[string]*AuthRequest - codes map[string]string - tokens map[string]*Token - clients map[string]*Client - users map[string]*User - services map[string]Service - refreshTokens map[string]*RefreshToken - signingKey signingKey -} - -type signingKey struct { - ID string - Algorithm string - Key *rsa.PrivateKey -} - -func NewStorage() *storage { - key, _ := rsa.GenerateKey(rand.Reader, 2048) - return &storage{ - authRequests: make(map[string]*AuthRequest), - codes: make(map[string]string), - tokens: make(map[string]*Token), - refreshTokens: make(map[string]*RefreshToken), - clients: clients, - users: map[string]*User{ - "id1": { - id: "id1", - username: "test-user", - password: "verysecure", - firstname: "Test", - lastname: "User", - email: "test-user@zitadel.ch", - emailVerified: true, - phone: "", - phoneVerified: false, - preferredLanguage: language.German, - }, - }, - services: map[string]Service{ - "service": { - keys: map[string]*rsa.PublicKey{ - "key1": serviceKey1, - }, - }, - }, - signingKey: signingKey{ - ID: "id", - Algorithm: "RS256", - Key: key, - }, - } -} - -//CheckUsernamePassword implements the `authenticate` interface of the login -func (s *storage) CheckUsernamePassword(username, password, id string) error { - request, ok := s.authRequests[id] - if !ok { - return fmt.Errorf("request not found") - } - - //for demonstration purposes we'll check on a static list with plain text password - //for real world scenarios, be sure to have the password hashed and salted (e.g. using bcrypt) - for _, user := range s.users { - if user.username == username && user.password == password { - //be sure to set user id into the auth request after the user was checked, - //so that you'll be able to get more information about the user after the login - request.UserID = user.id - - //you will have to change some state on the request to guide the user through possible multiple steps of the login process - //in this example we'll simply check the username / password and set a boolean to true - //therefore we will also just check this boolean if the request / login has been finished - request.passwordChecked = true - return nil - } - } - return fmt.Errorf("username or password wrong") -} - -//CreateAuthRequest implements the op.Storage interface -//it will be called after parsing and validation of the authentication request -func (s *storage) CreateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, userID string) (op.AuthRequest, error) { - //typically, you'll fill your internal / storage model with the information of the passed object - request := authRequestToInternal(authReq, userID) - - //you'll also have to create a unique id for the request (this might be done by your database; we'll use a uuid) - request.ID = uuid.NewString() - - //and save it in your database (for demonstration purposed we will use a simple map) - s.authRequests[request.ID] = request - - //finally, return the request (which implements the AuthRequest interface of the OP - return request, nil -} - -//AuthRequestByID implements the op.Storage interface -//it will be called after the Login UI redirects back to the OIDC endpoint -func (s *storage) AuthRequestByID(ctx context.Context, id string) (op.AuthRequest, error) { - request, ok := s.authRequests[id] - if !ok { - return nil, fmt.Errorf("request not found") - } - return request, nil -} - -//AuthRequestByCode implements the op.Storage interface -//it will be called after parsing and validation of the token request (in an authorization code flow) -func (s *storage) AuthRequestByCode(ctx context.Context, code string) (op.AuthRequest, error) { - //for this example we read the id by code and then get the request by id - requestID, ok := s.codes[code] - if !ok { - return nil, fmt.Errorf("code invalid or expired") - } - return s.AuthRequestByID(ctx, requestID) -} - -//SaveAuthCode implements the op.Storage interface -//it will be called after the authentication has been successful and before redirecting the user agent to the redirect_uri -//(in an authorization code flow) -func (s *storage) SaveAuthCode(ctx context.Context, id string, code string) error { - //for this example we'll just save the authRequestID to the code - s.codes[code] = id - return nil -} - -//DeleteAuthRequest implements the op.Storage interface -//it will be called after creating the token response (id and access tokens) for a valid -//- authentication request (in an implicit flow) -//- token request (in an authorization code flow) -func (s *storage) DeleteAuthRequest(ctx context.Context, id string) error { - //you can simply delete all reference to the auth request - delete(s.authRequests, id) - for code, requestID := range s.codes { - if id == requestID { - delete(s.codes, code) - return nil - } - } - return nil -} - -//CreateAccessToken implements the op.Storage interface -//it will be called for all requests able to return an access token (Authorization Code Flow, Implicit Flow, JWT Profile, ...) -func (s *storage) CreateAccessToken(ctx context.Context, request op.TokenRequest) (string, time.Time, error) { - var applicationID string - //if authenticated for an app (auth code / implicit flow) we must save the client_id to the token - authReq, ok := request.(*AuthRequest) - if ok { - applicationID = authReq.ApplicationID - } - token, err := s.accessToken(applicationID, "", request.GetSubject(), request.GetAudience(), request.GetScopes()) - if err != nil { - return "", time.Time{}, err - } - return token.ID, token.Expiration, nil -} - -//CreateAccessAndRefreshTokens implements the op.Storage interface -//it will be called for all requests able to return an access and refresh token (Authorization Code Flow, Refresh Token Request) -func (s *storage) CreateAccessAndRefreshTokens(ctx context.Context, request op.TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) { - //get the information depending on the request type / implementation - applicationID, authTime, amr := getInfoFromRequest(request) - - //if currentRefreshToken is empty (Code Flow) we will have to create a new refresh token - if currentRefreshToken == "" { - refreshTokenID := uuid.NewString() - accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes()) - if err != nil { - return "", "", time.Time{}, err - } - refreshToken, err := s.createRefreshToken(accessToken, amr, authTime) - if err != nil { - return "", "", time.Time{}, err - } - return accessToken.ID, refreshToken, accessToken.Expiration, nil - } - - //if we get here, the currentRefreshToken was not empty, so the call is a refresh token request - //we therefore will have to check the currentRefreshToken and renew the refresh token - refreshToken, refreshTokenID, err := s.renewRefreshToken(currentRefreshToken) - if err != nil { - return "", "", time.Time{}, err - } - accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes()) - if err != nil { - return "", "", time.Time{}, err - } - return accessToken.ID, refreshToken, accessToken.Expiration, nil -} - -//TokenRequestByRefreshToken implements the op.Storage interface -//it will be called after parsing and validation of the refresh token request -func (s *storage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (op.RefreshTokenRequest, error) { - token, ok := s.refreshTokens[refreshToken] - if !ok { - return nil, fmt.Errorf("invalid refresh_token") - } - return RefreshTokenRequestFromBusiness(token), nil -} - -//TerminateSession implements the op.Storage interface -//it will be called after the user signed out, therefore the access and refresh token of the user of this client must be removed -func (s *storage) TerminateSession(ctx context.Context, userID string, clientID string) error { - for _, token := range s.tokens { - if token.ApplicationID == clientID && token.Subject == userID { - delete(s.tokens, token.ID) - delete(s.refreshTokens, token.RefreshTokenID) - return nil - } - } - return nil -} - -//RevokeToken implements the op.Storage interface -//it will be called after parsing and validation of the token revocation request -func (s *storage) RevokeToken(ctx context.Context, token string, userID string, clientID string) *oidc.Error { - //a single token was requested to be removed - accessToken, ok := s.tokens[token] - if ok { - if accessToken.ApplicationID != clientID { - return oidc.ErrInvalidClient().WithDescription("token was not issued for this client") - } - //if it is an access token, just remove it - //you could also remove the corresponding refresh token if really necessary - delete(s.tokens, accessToken.ID) - return nil - } - refreshToken, ok := s.refreshTokens[token] - if !ok { - //if the token is neither an access nor a refresh token, just ignore it, the expected behaviour of - //being not valid (anymore) is achieved - return nil - } - if refreshToken.ApplicationID != clientID { - return oidc.ErrInvalidClient().WithDescription("token was not issued for this client") - } - //if it is a refresh token, you will have to remove the access token as well - delete(s.refreshTokens, refreshToken.ID) - for _, accessToken := range s.tokens { - if accessToken.RefreshTokenID == refreshToken.ID { - delete(s.tokens, accessToken.ID) - return nil - } - } - return nil -} - -//GetSigningKey implements the op.Storage interface -//it will be called when creating the OpenID Provider -func (s *storage) GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey) { - //in this example the signing key is a static rsa.PrivateKey and the algorithm used is RS256 - //you would obviously have a more complex implementation and store / retrieve the key from your database as well - // - //the idea of the signing key channel is, that you can (with what ever mechanism) rotate your signing key and - //switch the key of the signer via this channel - keyCh <- jose.SigningKey{ - Algorithm: jose.SignatureAlgorithm(s.signingKey.Algorithm), //always tell the signer with algorithm to use - Key: jose.JSONWebKey{ - KeyID: s.signingKey.ID, //always give the key an id so, that it will include it in the token header as `kid` claim - Key: s.signingKey.Key, - }, - } -} - -//GetKeySet implements the op.Storage interface -//it will be called to get the current (public) keys, among others for the keys_endpoint or for validating access_tokens on the userinfo_endpoint, ... -func (s *storage) GetKeySet(ctx context.Context) (*jose.JSONWebKeySet, error) { - //as mentioned above, this example only has a single signing key without key rotation, - //so it will directly use its public key - // - //when using key rotation you typically would store the public keys alongside the private keys in your database - //and give both of them an expiration date, with the public key having a longer lifetime (e.g. rotate private key every - return &jose.JSONWebKeySet{Keys: []jose.JSONWebKey{ - { - KeyID: s.signingKey.ID, - Algorithm: s.signingKey.Algorithm, - Use: oidc.KeyUseSignature, - Key: &s.signingKey.Key.PublicKey, - }}, - }, nil -} - -//GetClientByClientID implements the op.Storage interface -//it will be called whenever information (type, redirect_uris, ...) about the client behind the client_id is needed -func (s *storage) GetClientByClientID(ctx context.Context, clientID string) (op.Client, error) { - client, ok := s.clients[clientID] - if !ok { - return nil, fmt.Errorf("client not found") - } - return client, nil -} - -//AuthorizeClientIDSecret implements the op.Storage interface -//it will be called for validating the client_id, client_secret on token or introspection requests -func (s *storage) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error { - client, ok := s.clients[clientID] - if !ok { - return fmt.Errorf("client not found") - } - //for this example we directly check the secret - //obviously you would not have the secret in plain text, but rather hashed and salted (e.g. using bcrypt) - if client.secret != clientSecret { - return fmt.Errorf("invalid secret") - } - return nil -} - -//SetUserinfoFromScopes implements the op.Storage interface -//it will be called for the creation of an id_token, so we'll just pass it to the private function without any further check -func (s *storage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error { - return s.setUserinfo(ctx, userinfo, userID, clientID, scopes) -} - -//SetUserinfoFromToken implements the op.Storage interface -//it will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function -func (s *storage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error { - token, ok := s.tokens[tokenID] - if !ok { - return fmt.Errorf("token is invalid or has expired") - } - //the userinfo endpoint should support CORS. If it's not possible to specify a specific origin in the CORS handler, - //and you have to specify a wildcard (*) origin, then you could also check here if the origin which called the userinfo endpoint here directly - //note that the origin can be empty (if called by a web client) - // - //if origin != "" { - // client, ok := s.clients[token.ApplicationID] - // if !ok { - // return fmt.Errorf("client not found") - // } - // if err := checkAllowedOrigins(client.allowedOrigins, origin); err != nil { - // return err - // } - //} - return s.setUserinfo(ctx, userinfo, token.Subject, token.ApplicationID, token.Scopes) -} - -//SetIntrospectionFromToken implements the op.Storage interface -//it will be called for the introspection endpoint, so we read the token and pass the information from that to the private function -func (s *storage) SetIntrospectionFromToken(ctx context.Context, introspection oidc.IntrospectionResponse, tokenID, subject, clientID string) error { - token, ok := s.tokens[tokenID] - if !ok { - return fmt.Errorf("token is invalid or has expired") - } - //check if the client is part of the requested audience - for _, aud := range token.Audience { - if aud == clientID { - //the introspection response only has to return a boolean (active) if the token is active - //this will automatically be done by the library if you don't return an error - //you can also return further information about the user / associated token - //e.g. the userinfo (equivalent to userinfo endpoint) - err := s.setUserinfo(ctx, introspection, subject, clientID, token.Scopes) - if err != nil { - return err - } - //...and also the requested scopes... - introspection.SetScopes(token.Scopes) - //...and the client the token was issued to - introspection.SetClientID(token.ApplicationID) - return nil - } - } - return fmt.Errorf("token is not valid for this client") -} - -//GetPrivateClaimsFromScopes implements the op.Storage interface -//it will be called for the creation of a JWT access token to assert claims for custom scopes -func (s *storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) { - for _, scope := range scopes { - switch scope { - case CustomScope: - claims = appendClaim(claims, CustomClaim, customClaim(clientID)) - } - } - return claims, nil -} - -//GetKeyByIDAndUserID implements the op.Storage interface -//it will be called to validate the signatures of a JWT (JWT Profile Grant and Authentication) -func (s *storage) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) { - service, ok := s.services[userID] - if !ok { - return nil, fmt.Errorf("user not found") - } - key, ok := service.keys[keyID] - if !ok { - return nil, fmt.Errorf("key not found") - } - return &jose.JSONWebKey{ - KeyID: keyID, - Use: "sig", - Key: key, - }, nil -} - -//ValidateJWTProfileScopes implements the op.Storage interface -//it will be called to validate the scopes of a JWT Profile Authorization Grant request -func (s *storage) ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error) { - allowedScopes := make([]string, 0) - for _, scope := range scopes { - if scope == oidc.ScopeOpenID { - allowedScopes = append(allowedScopes, scope) - } - } - return allowedScopes, nil -} - -//Health implements the op.Storage interface -func (s *storage) Health(ctx context.Context) error { - return nil -} - -//createRefreshToken will store a refresh_token in-memory based on the provided information -func (s *storage) createRefreshToken(accessToken *Token, amr []string, authTime time.Time) (string, error) { - token := &RefreshToken{ - ID: accessToken.RefreshTokenID, - Token: accessToken.RefreshTokenID, - AuthTime: authTime, - AMR: amr, - ApplicationID: accessToken.ApplicationID, - UserID: accessToken.Subject, - Audience: accessToken.Audience, - Expiration: time.Now().Add(5 * time.Hour), - Scopes: accessToken.Scopes, - } - s.refreshTokens[token.ID] = token - return token.Token, nil -} - -//renewRefreshToken checks the provided refresh_token and creates a new one based on the current -func (s *storage) renewRefreshToken(currentRefreshToken string) (string, string, error) { - refreshToken, ok := s.refreshTokens[currentRefreshToken] - if !ok { - return "", "", fmt.Errorf("invalid refresh token") - } - //deletes the refresh token and all access tokens which were issued based on this refresh token - delete(s.refreshTokens, currentRefreshToken) - for _, token := range s.tokens { - if token.RefreshTokenID == currentRefreshToken { - delete(s.tokens, token.ID) - break - } - } - //creates a new refresh token based on the current one - token := uuid.NewString() - refreshToken.Token = token - s.refreshTokens[token] = refreshToken - return token, refreshToken.ID, nil -} - -//accessToken will store an access_token in-memory based on the provided information -func (s *storage) accessToken(applicationID, refreshTokenID, subject string, audience, scopes []string) (*Token, error) { - token := &Token{ - ID: uuid.NewString(), - ApplicationID: applicationID, - RefreshTokenID: refreshTokenID, - Subject: subject, - Audience: audience, - Expiration: time.Now().Add(5 * time.Minute), - Scopes: scopes, - } - s.tokens[token.ID] = token - return token, nil -} - -//setUserinfo sets the info based on the user, scopes and if necessary the clientID -func (s *storage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSetter, userID, clientID string, scopes []string) (err error) { - user, ok := s.users[userID] - if !ok { - return fmt.Errorf("user not found") - } - for _, scope := range scopes { - switch scope { - case oidc.ScopeOpenID: - userInfo.SetSubject(user.id) - case oidc.ScopeEmail: - userInfo.SetEmail(user.email, user.emailVerified) - case oidc.ScopeProfile: - userInfo.SetPreferredUsername(user.username) - userInfo.SetName(user.firstname + " " + user.lastname) - userInfo.SetFamilyName(user.lastname) - userInfo.SetGivenName(user.firstname) - userInfo.SetLocale(user.preferredLanguage) - case oidc.ScopePhone: - userInfo.SetPhone(user.phone, user.phoneVerified) - case CustomScope: - //you can also have a custom scope and assert public or custom claims based on that - userInfo.AppendClaims(CustomClaim, customClaim(clientID)) - } - } - return nil -} - -//getInfoFromRequest returns the clientID, authTime and amr depending on the op.TokenRequest type / implementation -func getInfoFromRequest(req op.TokenRequest) (clientID string, authTime time.Time, amr []string) { - authReq, ok := req.(*AuthRequest) //Code Flow (with scope offline_access) - if ok { - return authReq.ApplicationID, authReq.authTime, authReq.GetAMR() - } - refreshReq, ok := req.(*RefreshTokenRequest) //Refresh Token Request - if ok { - return refreshReq.ApplicationID, refreshReq.AuthTime, refreshReq.AMR - } - return "", time.Time{}, nil -} - -//customClaim demonstrates how to return custom claims based on provided information -func customClaim(clientID string) map[string]interface{} { - return map[string]interface{}{ - "client": clientID, - "other": "stuff", - } -} - -func appendClaim(claims map[string]interface{}, claim string, value interface{}) map[string]interface{} { - if claims == nil { - claims = make(map[string]interface{}) - } - claims[claim] = value - return claims -} diff --git a/example/server/internal/user.go b/example/server/internal/user.go deleted file mode 100644 index 19b5d1f..0000000 --- a/example/server/internal/user.go +++ /dev/null @@ -1,24 +0,0 @@ -package internal - -import ( - "crypto/rsa" - - "golang.org/x/text/language" -) - -type User struct { - id string - username string - password string - firstname string - lastname string - email string - emailVerified bool - phone string - phoneVerified bool - preferredLanguage language.Tag -} - -type Service struct { - keys map[string]*rsa.PublicKey -} diff --git a/example/server/main.go b/example/server/main.go new file mode 100644 index 0000000..37fbcb3 --- /dev/null +++ b/example/server/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "context" + "log" + "net/http" + + "github.com/zitadel/oidc/example/server/exampleop" + "github.com/zitadel/oidc/example/server/storage" +) + +func main() { + ctx := context.Background() + + // the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations + // this might be the layer for accessing your database + // in this example it will be handled in-memory + storage := storage.NewStorage(storage.NewUserStore()) + + port := "9998" + router := exampleop.SetupServer(ctx, "http://localhost:"+port, storage) + + server := &http.Server{ + Addr: ":" + port, + Handler: router, + } + err := server.ListenAndServe() + if err != nil { + log.Fatal(err) + } + <-ctx.Done() +} diff --git a/example/server/op.go b/example/server/op.go deleted file mode 100644 index d689247..0000000 --- a/example/server/op.go +++ /dev/null @@ -1,126 +0,0 @@ -package main - -import ( - "context" - "crypto/sha256" - "fmt" - "log" - "net/http" - "os" - - "github.com/gorilla/mux" - "golang.org/x/text/language" - - "github.com/zitadel/oidc/example/server/internal" - "github.com/zitadel/oidc/pkg/op" -) - -const ( - pathLoggedOut = "/logged-out" -) - -func init() { - internal.RegisterClients( - internal.NativeClient("native"), - internal.WebClient("web", "secret"), - internal.WebClient("api", "secret"), - ) -} - -func main() { - ctx := context.Background() - - //this will allow us to use an issuer with http:// instead of https:// - os.Setenv(op.OidcDevMode, "true") - - port := "9998" - - //the OpenID Provider requires a 32-byte key for (token) encryption - //be sure to create a proper crypto random key and manage it securely! - key := sha256.Sum256([]byte("test")) - - router := mux.NewRouter() - - //for simplicity, we provide a very small default page for users who have signed out - router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) { - _, err := w.Write([]byte("signed out successfully")) - if err != nil { - log.Printf("error serving logged out page: %v", err) - } - }) - - //the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations - //this might be the layer for accessing your database - //in this example it will be handled in-memory - storage := internal.NewStorage() - - //creation of the OpenIDProvider with the just created in-memory Storage - provider, err := newOP(ctx, storage, port, key) - if err != nil { - log.Fatal(err) - } - - //the provider will only take care of the OpenID Protocol, so there must be some sort of UI for the login process - //for the simplicity of the example this means a simple page with username and password field - l := NewLogin(storage, op.AuthCallbackURL(provider)) - - //regardless of how many pages / steps there are in the process, the UI must be registered in the router, - //so we will direct all calls to /login to the login UI - router.PathPrefix("/login/").Handler(http.StripPrefix("/login", l.router)) - - //we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration) - //is served on the correct path - // - //if your issuer ends with a path (e.g. http://localhost:9998/custom/path/), - //then you would have to set the path prefix (/custom/path/) - router.PathPrefix("/").Handler(provider.HttpHandler()) - - server := &http.Server{ - Addr: ":" + port, - Handler: router, - } - err = server.ListenAndServe() - if err != nil { - log.Fatal(err) - } - <-ctx.Done() -} - -//newOP will create an OpenID Provider for localhost on a specified port with a given encryption key -//and a predefined default logout uri -//it will enable all options (see descriptions) -func newOP(ctx context.Context, storage op.Storage, port string, key [32]byte) (op.OpenIDProvider, error) { - config := &op.Config{ - Issuer: fmt.Sprintf("http://localhost:%s/", port), - CryptoKey: key, - - //will be used if the end_session endpoint is called without a post_logout_redirect_uri - DefaultLogoutRedirectURI: pathLoggedOut, - - //enables code_challenge_method S256 for PKCE (and therefore PKCE in general) - CodeMethodS256: true, - - //enables additional client_id/client_secret authentication by form post (not only HTTP Basic Auth) - AuthMethodPost: true, - - //enables additional authentication by using private_key_jwt - AuthMethodPrivateKeyJWT: true, - - //enables refresh_token grant use - GrantTypeRefreshToken: true, - - //enables use of the `request` Object parameter - RequestObjectSupported: true, - - //this example has only static texts (in English), so we'll set the here accordingly - SupportedUILocales: []language.Tag{language.English}, - } - handler, err := op.NewOpenIDProvider(ctx, config, storage, - //as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth - op.WithCustomAuthEndpoint(op.NewEndpoint("auth")), - ) - if err != nil { - return nil, err - } - return handler, nil -} diff --git a/example/server/internal/client.go b/example/server/storage/client.go similarity index 60% rename from example/server/internal/client.go rename to example/server/storage/client.go index 9080e8c..0f3a703 100644 --- a/example/server/internal/client.go +++ b/example/server/storage/client.go @@ -1,4 +1,4 @@ -package internal +package storage import ( "time" @@ -8,17 +8,17 @@ import ( ) var ( - //we use the default login UI and pass the (auth request) id + // we use the default login UI and pass the (auth request) id defaultLoginURL = func(id string) string { return "/login/username?authRequestID=" + id } - //clients to be used by the storage interface + // clients to be used by the storage interface clients = map[string]*Client{} ) -//Client represents the internal model of an OAuth/OIDC client -//this could also be your database model +// Client represents the storage model of an OAuth/OIDC client +// this could also be your database model type Client struct { id string secret string @@ -34,108 +34,111 @@ type Client struct { clockSkew time.Duration } -//GetID must return the client_id +// GetID must return the client_id func (c *Client) GetID() string { return c.id } -//RedirectURIs must return the registered redirect_uris for Code and Implicit Flow +// RedirectURIs must return the registered redirect_uris for Code and Implicit Flow func (c *Client) RedirectURIs() []string { return c.redirectURIs } -//PostLogoutRedirectURIs must return the registered post_logout_redirect_uris for sign-outs +// PostLogoutRedirectURIs must return the registered post_logout_redirect_uris for sign-outs func (c *Client) PostLogoutRedirectURIs() []string { return []string{} } -//ApplicationType must return the type of the client (app, native, user agent) +// ApplicationType must return the type of the client (app, native, user agent) func (c *Client) ApplicationType() op.ApplicationType { return c.applicationType } -//AuthMethod must return the authentication method (client_secret_basic, client_secret_post, none, private_key_jwt) +// AuthMethod must return the authentication method (client_secret_basic, client_secret_post, none, private_key_jwt) func (c *Client) AuthMethod() oidc.AuthMethod { return c.authMethod } -//ResponseTypes must return all allowed response types (code, id_token token, id_token) -//these must match with the allowed grant types +// ResponseTypes must return all allowed response types (code, id_token token, id_token) +// these must match with the allowed grant types func (c *Client) ResponseTypes() []oidc.ResponseType { return c.responseTypes } -//GrantTypes must return all allowed grant types (authorization_code, refresh_token, urn:ietf:params:oauth:grant-type:jwt-bearer) +// GrantTypes must return all allowed grant types (authorization_code, refresh_token, urn:ietf:params:oauth:grant-type:jwt-bearer) func (c *Client) GrantTypes() []oidc.GrantType { return c.grantTypes } -//LoginURL will be called to redirect the user (agent) to the login UI -//you could implement some logic here to redirect the users to different login UIs depending on the client +// LoginURL will be called to redirect the user (agent) to the login UI +// you could implement some logic here to redirect the users to different login UIs depending on the client func (c *Client) LoginURL(id string) string { return c.loginURL(id) } -//AccessTokenType must return the type of access token the client uses (Bearer (opaque) or JWT) +// AccessTokenType must return the type of access token the client uses (Bearer (opaque) or JWT) func (c *Client) AccessTokenType() op.AccessTokenType { return c.accessTokenType } -//IDTokenLifetime must return the lifetime of the client's id_tokens +// IDTokenLifetime must return the lifetime of the client's id_tokens func (c *Client) IDTokenLifetime() time.Duration { return 1 * time.Hour } -//DevMode enables the use of non-compliant configs such as redirect_uris (e.g. http schema for user agent client) +// DevMode enables the use of non-compliant configs such as redirect_uris (e.g. http schema for user agent client) func (c *Client) DevMode() bool { return c.devMode } -//RestrictAdditionalIdTokenScopes allows specifying which custom scopes shall be asserted into the id_token +// RestrictAdditionalIdTokenScopes allows specifying which custom scopes shall be asserted into the id_token func (c *Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []string { return func(scopes []string) []string { return scopes } } -//RestrictAdditionalAccessTokenScopes allows specifying which custom scopes shall be asserted into the JWT access_token +// RestrictAdditionalAccessTokenScopes allows specifying which custom scopes shall be asserted into the JWT access_token func (c *Client) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { return func(scopes []string) []string { return scopes } } -//IsScopeAllowed enables Client specific custom scopes validation -//in this example we allow the CustomScope for all clients +// IsScopeAllowed enables Client specific custom scopes validation +// in this example we allow the CustomScope for all clients func (c *Client) IsScopeAllowed(scope string) bool { return scope == CustomScope } -//IDTokenUserinfoClaimsAssertion allows specifying if claims of scope profile, email, phone and address are asserted into the id_token -//even if an access token if issued which violates the OIDC Core spec +// IDTokenUserinfoClaimsAssertion allows specifying if claims of scope profile, email, phone and address are asserted into the id_token +// even if an access token if issued which violates the OIDC Core spec //(5.4. Requesting Claims using Scope Values: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) -//some clients though require that e.g. email is always in the id_token when requested even if an access_token is issued +// some clients though require that e.g. email is always in the id_token when requested even if an access_token is issued func (c *Client) IDTokenUserinfoClaimsAssertion() bool { return c.idTokenUserinfoClaimsAssertion } -//ClockSkew enables clients to instruct the OP to apply a clock skew on the various times and expirations +// ClockSkew enables clients to instruct the OP to apply a clock skew on the various times and expirations //(subtract from issued_at, add to expiration, ...) func (c *Client) ClockSkew() time.Duration { return c.clockSkew } -//RegisterClients enables you to register clients for the example implementation -//there are some clients (web and native) to try out different cases -//add more if necessary +// RegisterClients enables you to register clients for the example implementation +// there are some clients (web and native) to try out different cases +// add more if necessary +// +// RegisterClients should be called before the Storage is used so that there are +// no race conditions. func RegisterClients(registerClients ...*Client) { for _, client := range registerClients { clients[client.id] = client } } -//NativeClient will create a client of type native, which will always use PKCE and allow the use of refresh tokens -//user-defined redirectURIs may include: +// NativeClient will create a client of type native, which will always use PKCE and allow the use of refresh tokens +// user-defined redirectURIs may include: // - http://localhost without port specification (e.g. http://localhost/auth/callback) // - custom protocol (e.g. custom://auth/callback) //(the examples will be used as default, if none is provided) @@ -148,7 +151,7 @@ func NativeClient(id string, redirectURIs ...string) *Client { } return &Client{ id: id, - secret: "", //no secret needed (due to PKCE) + secret: "", // no secret needed (due to PKCE) redirectURIs: redirectURIs, applicationType: op.ApplicationTypeNative, authMethod: oidc.AuthMethodNone, @@ -162,8 +165,8 @@ func NativeClient(id string, redirectURIs ...string) *Client { } } -//WebClient will create a client of type web, which will always use Basic Auth and allow the use of refresh tokens -//user-defined redirectURIs may include: +// WebClient will create a client of type web, which will always use Basic Auth and allow the use of refresh tokens +// user-defined redirectURIs may include: // - http://localhost with port specification (e.g. http://localhost:9999/auth/callback) //(the example will be used as default, if none is provided) func WebClient(id, secret string, redirectURIs ...string) *Client { diff --git a/example/server/internal/oidc.go b/example/server/storage/oidc.go similarity index 87% rename from example/server/internal/oidc.go rename to example/server/storage/oidc.go index 5edf970..91afd90 100644 --- a/example/server/internal/oidc.go +++ b/example/server/storage/oidc.go @@ -1,4 +1,4 @@ -package internal +package storage import ( "time" @@ -11,11 +11,11 @@ import ( ) const ( - //CustomScope is an example for how to use custom scopes in this library + // CustomScope is an example for how to use custom scopes in this library //(in this scenario, when requested, it will return a custom claim) CustomScope = "custom_scope" - //CustomClaim is an example for how to return custom claims with this library + // CustomClaim is an example for how to return custom claims with this library CustomClaim = "custom_claim" ) @@ -44,11 +44,11 @@ func (a *AuthRequest) GetID() string { } func (a *AuthRequest) GetACR() string { - return "" //we won't handle acr in this example + return "" // we won't handle acr in this example } func (a *AuthRequest) GetAMR() []string { - //this example only uses password for authentication + // this example only uses password for authentication if a.passwordChecked { return []string{"pwd"} } @@ -56,7 +56,7 @@ func (a *AuthRequest) GetAMR() []string { } func (a *AuthRequest) GetAudience() []string { - return []string{a.ApplicationID} //this example will always just use the client_id as audience + return []string{a.ApplicationID} // this example will always just use the client_id as audience } func (a *AuthRequest) GetAuthTime() time.Time { @@ -84,7 +84,7 @@ func (a *AuthRequest) GetResponseType() oidc.ResponseType { } func (a *AuthRequest) GetResponseMode() oidc.ResponseMode { - return "" //we won't handle response mode in this example + return "" // we won't handle response mode in this example } func (a *AuthRequest) GetScopes() []string { @@ -100,7 +100,7 @@ func (a *AuthRequest) GetSubject() string { } func (a *AuthRequest) Done() bool { - return a.passwordChecked //this example only uses password for authentication + return a.passwordChecked // this example only uses password for authentication } func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string { @@ -165,7 +165,7 @@ func CodeChallengeToOIDC(challenge *OIDCCodeChallenge) *oidc.CodeChallenge { } } -//RefreshTokenRequestFromBusiness will simply wrap the internal RefreshToken to implement the op.RefreshTokenRequest interface +// RefreshTokenRequestFromBusiness will simply wrap the storage RefreshToken to implement the op.RefreshTokenRequest interface func RefreshTokenRequestFromBusiness(token *RefreshToken) op.RefreshTokenRequest { return &RefreshTokenRequest{token} } diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go new file mode 100644 index 0000000..7b9d413 --- /dev/null +++ b/example/server/storage/storage.go @@ -0,0 +1,590 @@ +package storage + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "fmt" + "math/big" + "sync" + "time" + + "github.com/google/uuid" + "gopkg.in/square/go-jose.v2" + + "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/pkg/op" +) + +// serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant +// the corresponding private key is in the service-key1.json (for demonstration purposes) +var serviceKey1 = &rsa.PublicKey{ + N: func() *big.Int { + n, _ := new(big.Int).SetString("00f6d44fb5f34ac2033a75e73cb65ff24e6181edc58845e75a560ac21378284977bb055b1a75b714874e2a2641806205681c09abec76efd52cf40984edcf4c8ca09717355d11ac338f280d3e4c905b00543bdb8ee5a417496cb50cb0e29afc5a0d0471fd5a2fa625bd5281f61e6b02067d4fe7a5349eeae6d6a4300bcd86eef331", 16) + return n + }(), + E: 65537, +} + +// var _ op.Storage = &storage{} +// var _ op.ClientCredentialsStorage = &storage{} + +// storage implements the op.Storage interface +// typically you would implement this as a layer on top of your database +// for simplicity this example keeps everything in-memory +type Storage struct { + lock sync.Mutex + authRequests map[string]*AuthRequest + codes map[string]string + tokens map[string]*Token + clients map[string]*Client + userStore UserStore + services map[string]Service + refreshTokens map[string]*RefreshToken + signingKey signingKey +} + +type signingKey struct { + ID string + Algorithm string + Key *rsa.PrivateKey +} + +func NewStorage(userStore UserStore) *Storage { + key, _ := rsa.GenerateKey(rand.Reader, 2048) + return &Storage{ + authRequests: make(map[string]*AuthRequest), + codes: make(map[string]string), + tokens: make(map[string]*Token), + refreshTokens: make(map[string]*RefreshToken), + clients: clients, + userStore: userStore, + services: map[string]Service{ + userStore.ExampleClientID(): { + keys: map[string]*rsa.PublicKey{ + "key1": serviceKey1, + }, + }, + }, + signingKey: signingKey{ + ID: "id", + Algorithm: "RS256", + Key: key, + }, + } +} + +// CheckUsernamePassword implements the `authenticate` interface of the login +func (s *Storage) CheckUsernamePassword(username, password, id string) error { + s.lock.Lock() + defer s.lock.Unlock() + request, ok := s.authRequests[id] + if !ok { + return fmt.Errorf("request not found") + } + + // for demonstration purposes we'll check we'll have a simple user store and + // a plain text password. For real world scenarios, be sure to have the password + // hashed and salted (e.g. using bcrypt) + user := s.userStore.GetUserByUsername(username) + if user != nil && user.Password == password { + // be sure to set user id into the auth request after the user was checked, + // so that you'll be able to get more information about the user after the login + request.UserID = user.ID + + // you will have to change some state on the request to guide the user through possible multiple steps of the login process + // in this example we'll simply check the username / password and set a boolean to true + // therefore we will also just check this boolean if the request / login has been finished + request.passwordChecked = true + return nil + } + return fmt.Errorf("username or password wrong") +} + +// CreateAuthRequest implements the op.Storage interface +// it will be called after parsing and validation of the authentication request +func (s *Storage) CreateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, userID string) (op.AuthRequest, error) { + s.lock.Lock() + defer s.lock.Unlock() + + // typically, you'll fill your storage / storage model with the information of the passed object + request := authRequestToInternal(authReq, userID) + + // you'll also have to create a unique id for the request (this might be done by your database; we'll use a uuid) + request.ID = uuid.NewString() + + // and save it in your database (for demonstration purposed we will use a simple map) + s.authRequests[request.ID] = request + + // finally, return the request (which implements the AuthRequest interface of the OP + return request, nil +} + +// AuthRequestByID implements the op.Storage interface +// it will be called after the Login UI redirects back to the OIDC endpoint +func (s *Storage) AuthRequestByID(ctx context.Context, id string) (op.AuthRequest, error) { + s.lock.Lock() + defer s.lock.Unlock() + request, ok := s.authRequests[id] + if !ok { + return nil, fmt.Errorf("request not found") + } + return request, nil +} + +// AuthRequestByCode implements the op.Storage interface +// it will be called after parsing and validation of the token request (in an authorization code flow) +func (s *Storage) AuthRequestByCode(ctx context.Context, code string) (op.AuthRequest, error) { + // for this example we read the id by code and then get the request by id + requestID, ok := func() (string, bool) { + s.lock.Lock() + defer s.lock.Unlock() + requestID, ok := s.codes[code] + return requestID, ok + }() + if !ok { + return nil, fmt.Errorf("code invalid or expired") + } + return s.AuthRequestByID(ctx, requestID) +} + +// SaveAuthCode implements the op.Storage interface +// it will be called after the authentication has been successful and before redirecting the user agent to the redirect_uri +//(in an authorization code flow) +func (s *Storage) SaveAuthCode(ctx context.Context, id string, code string) error { + // for this example we'll just save the authRequestID to the code + s.lock.Lock() + defer s.lock.Unlock() + s.codes[code] = id + return nil +} + +// DeleteAuthRequest implements the op.Storage interface +// it will be called after creating the token response (id and access tokens) for a valid +//- authentication request (in an implicit flow) +//- token request (in an authorization code flow) +func (s *Storage) DeleteAuthRequest(ctx context.Context, id string) error { + // you can simply delete all reference to the auth request + s.lock.Lock() + defer s.lock.Unlock() + delete(s.authRequests, id) + for code, requestID := range s.codes { + if id == requestID { + delete(s.codes, code) + return nil + } + } + return nil +} + +// CreateAccessToken implements the op.Storage interface +// it will be called for all requests able to return an access token (Authorization Code Flow, Implicit Flow, JWT Profile, ...) +func (s *Storage) CreateAccessToken(ctx context.Context, request op.TokenRequest) (string, time.Time, error) { + var applicationID string + // if authenticated for an app (auth code / implicit flow) we must save the client_id to the token + authReq, ok := request.(*AuthRequest) + if ok { + applicationID = authReq.ApplicationID + } + token, err := s.accessToken(applicationID, "", request.GetSubject(), request.GetAudience(), request.GetScopes()) + if err != nil { + return "", time.Time{}, err + } + return token.ID, token.Expiration, nil +} + +// CreateAccessAndRefreshTokens implements the op.Storage interface +// it will be called for all requests able to return an access and refresh token (Authorization Code Flow, Refresh Token Request) +func (s *Storage) CreateAccessAndRefreshTokens(ctx context.Context, request op.TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) { + // get the information depending on the request type / implementation + applicationID, authTime, amr := getInfoFromRequest(request) + + // if currentRefreshToken is empty (Code Flow) we will have to create a new refresh token + if currentRefreshToken == "" { + refreshTokenID := uuid.NewString() + accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes()) + if err != nil { + return "", "", time.Time{}, err + } + refreshToken, err := s.createRefreshToken(accessToken, amr, authTime) + if err != nil { + return "", "", time.Time{}, err + } + return accessToken.ID, refreshToken, accessToken.Expiration, nil + } + + // if we get here, the currentRefreshToken was not empty, so the call is a refresh token request + // we therefore will have to check the currentRefreshToken and renew the refresh token + refreshToken, refreshTokenID, err := s.renewRefreshToken(currentRefreshToken) + if err != nil { + return "", "", time.Time{}, err + } + accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes()) + if err != nil { + return "", "", time.Time{}, err + } + return accessToken.ID, refreshToken, accessToken.Expiration, nil +} + +// TokenRequestByRefreshToken implements the op.Storage interface +// it will be called after parsing and validation of the refresh token request +func (s *Storage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (op.RefreshTokenRequest, error) { + s.lock.Lock() + defer s.lock.Unlock() + token, ok := s.refreshTokens[refreshToken] + if !ok { + return nil, fmt.Errorf("invalid refresh_token") + } + return RefreshTokenRequestFromBusiness(token), nil +} + +// TerminateSession implements the op.Storage interface +// it will be called after the user signed out, therefore the access and refresh token of the user of this client must be removed +func (s *Storage) TerminateSession(ctx context.Context, userID string, clientID string) error { + s.lock.Lock() + defer s.lock.Unlock() + for _, token := range s.tokens { + if token.ApplicationID == clientID && token.Subject == userID { + delete(s.tokens, token.ID) + delete(s.refreshTokens, token.RefreshTokenID) + return nil + } + } + return nil +} + +// RevokeToken implements the op.Storage interface +// it will be called after parsing and validation of the token revocation request +func (s *Storage) RevokeToken(ctx context.Context, token string, userID string, clientID string) *oidc.Error { + // a single token was requested to be removed + s.lock.Lock() + defer s.lock.Unlock() + accessToken, ok := s.tokens[token] + if ok { + if accessToken.ApplicationID != clientID { + return oidc.ErrInvalidClient().WithDescription("token was not issued for this client") + } + // if it is an access token, just remove it + // you could also remove the corresponding refresh token if really necessary + delete(s.tokens, accessToken.ID) + return nil + } + refreshToken, ok := s.refreshTokens[token] + if !ok { + // if the token is neither an access nor a refresh token, just ignore it, the expected behaviour of + // being not valid (anymore) is achieved + return nil + } + if refreshToken.ApplicationID != clientID { + return oidc.ErrInvalidClient().WithDescription("token was not issued for this client") + } + // if it is a refresh token, you will have to remove the access token as well + delete(s.refreshTokens, refreshToken.ID) + for _, accessToken := range s.tokens { + if accessToken.RefreshTokenID == refreshToken.ID { + delete(s.tokens, accessToken.ID) + return nil + } + } + return nil +} + +// GetSigningKey implements the op.Storage interface +// it will be called when creating the OpenID Provider +func (s *Storage) GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey) { + // in this example the signing key is a static rsa.PrivateKey and the algorithm used is RS256 + // you would obviously have a more complex implementation and store / retrieve the key from your database as well + // + // the idea of the signing key channel is, that you can (with what ever mechanism) rotate your signing key and + // switch the key of the signer via this channel + keyCh <- jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(s.signingKey.Algorithm), // always tell the signer with algorithm to use + Key: jose.JSONWebKey{ + KeyID: s.signingKey.ID, // always give the key an id so, that it will include it in the token header as `kid` claim + Key: s.signingKey.Key, + }, + } +} + +// GetKeySet implements the op.Storage interface +// it will be called to get the current (public) keys, among others for the keys_endpoint or for validating access_tokens on the userinfo_endpoint, ... +func (s *Storage) GetKeySet(ctx context.Context) (*jose.JSONWebKeySet, error) { + // as mentioned above, this example only has a single signing key without key rotation, + // so it will directly use its public key + // + // when using key rotation you typically would store the public keys alongside the private keys in your database + // and give both of them an expiration date, with the public key having a longer lifetime (e.g. rotate private key every + return &jose.JSONWebKeySet{ + Keys: []jose.JSONWebKey{ + { + KeyID: s.signingKey.ID, + Algorithm: s.signingKey.Algorithm, + Use: oidc.KeyUseSignature, + Key: &s.signingKey.Key.PublicKey, + }, + }, + }, nil +} + +// GetClientByClientID implements the op.Storage interface +// it will be called whenever information (type, redirect_uris, ...) about the client behind the client_id is needed +func (s *Storage) GetClientByClientID(ctx context.Context, clientID string) (op.Client, error) { + s.lock.Lock() + defer s.lock.Unlock() + client, ok := s.clients[clientID] + if !ok { + return nil, fmt.Errorf("client not found") + } + return client, nil +} + +// AuthorizeClientIDSecret implements the op.Storage interface +// it will be called for validating the client_id, client_secret on token or introspection requests +func (s *Storage) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error { + s.lock.Lock() + defer s.lock.Unlock() + client, ok := s.clients[clientID] + if !ok { + return fmt.Errorf("client not found") + } + // for this example we directly check the secret + // obviously you would not have the secret in plain text, but rather hashed and salted (e.g. using bcrypt) + if client.secret != clientSecret { + return fmt.Errorf("invalid secret") + } + return nil +} + +// SetUserinfoFromScopes implements the op.Storage interface +// it will be called for the creation of an id_token, so we'll just pass it to the private function without any further check +func (s *Storage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error { + return s.setUserinfo(ctx, userinfo, userID, clientID, scopes) +} + +// SetUserinfoFromToken implements the op.Storage interface +// it will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function +func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error { + token, ok := func() (*Token, bool) { + s.lock.Lock() + defer s.lock.Unlock() + token, ok := s.tokens[tokenID] + return token, ok + }() + if !ok { + return fmt.Errorf("token is invalid or has expired") + } + // the userinfo endpoint should support CORS. If it's not possible to specify a specific origin in the CORS handler, + // and you have to specify a wildcard (*) origin, then you could also check here if the origin which called the userinfo endpoint here directly + // note that the origin can be empty (if called by a web client) + // + // if origin != "" { + // client, ok := s.clients[token.ApplicationID] + // if !ok { + // return fmt.Errorf("client not found") + // } + // if err := checkAllowedOrigins(client.allowedOrigins, origin); err != nil { + // return err + // } + //} + return s.setUserinfo(ctx, userinfo, token.Subject, token.ApplicationID, token.Scopes) +} + +// SetIntrospectionFromToken implements the op.Storage interface +// it will be called for the introspection endpoint, so we read the token and pass the information from that to the private function +func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection oidc.IntrospectionResponse, tokenID, subject, clientID string) error { + token, ok := func() (*Token, bool) { + s.lock.Lock() + defer s.lock.Unlock() + token, ok := s.tokens[tokenID] + return token, ok + }() + if !ok { + return fmt.Errorf("token is invalid or has expired") + } + // check if the client is part of the requested audience + for _, aud := range token.Audience { + if aud == clientID { + // the introspection response only has to return a boolean (active) if the token is active + // this will automatically be done by the library if you don't return an error + // you can also return further information about the user / associated token + // e.g. the userinfo (equivalent to userinfo endpoint) + err := s.setUserinfo(ctx, introspection, subject, clientID, token.Scopes) + if err != nil { + return err + } + //...and also the requested scopes... + introspection.SetScopes(token.Scopes) + //...and the client the token was issued to + introspection.SetClientID(token.ApplicationID) + return nil + } + } + return fmt.Errorf("token is not valid for this client") +} + +// GetPrivateClaimsFromScopes implements the op.Storage interface +// it will be called for the creation of a JWT access token to assert claims for custom scopes +func (s *Storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) { + for _, scope := range scopes { + switch scope { + case CustomScope: + claims = appendClaim(claims, CustomClaim, customClaim(clientID)) + } + } + return claims, nil +} + +// GetKeyByIDAndUserID implements the op.Storage interface +// it will be called to validate the signatures of a JWT (JWT Profile Grant and Authentication) +func (s *Storage) GetKeyByIDAndUserID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error) { + s.lock.Lock() + defer s.lock.Unlock() + service, ok := s.services[clientID] + if !ok { + return nil, fmt.Errorf("clientID not found") + } + key, ok := service.keys[keyID] + if !ok { + return nil, fmt.Errorf("key not found") + } + return &jose.JSONWebKey{ + KeyID: keyID, + Use: "sig", + Key: key, + }, nil +} + +// ValidateJWTProfileScopes implements the op.Storage interface +// it will be called to validate the scopes of a JWT Profile Authorization Grant request +func (s *Storage) ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error) { + allowedScopes := make([]string, 0) + for _, scope := range scopes { + if scope == oidc.ScopeOpenID { + allowedScopes = append(allowedScopes, scope) + } + } + return allowedScopes, nil +} + +// Health implements the op.Storage interface +func (s *Storage) Health(ctx context.Context) error { + return nil +} + +// createRefreshToken will store a refresh_token in-memory based on the provided information +func (s *Storage) createRefreshToken(accessToken *Token, amr []string, authTime time.Time) (string, error) { + s.lock.Lock() + defer s.lock.Unlock() + token := &RefreshToken{ + ID: accessToken.RefreshTokenID, + Token: accessToken.RefreshTokenID, + AuthTime: authTime, + AMR: amr, + ApplicationID: accessToken.ApplicationID, + UserID: accessToken.Subject, + Audience: accessToken.Audience, + Expiration: time.Now().Add(5 * time.Hour), + Scopes: accessToken.Scopes, + } + s.refreshTokens[token.ID] = token + return token.Token, nil +} + +// renewRefreshToken checks the provided refresh_token and creates a new one based on the current +func (s *Storage) renewRefreshToken(currentRefreshToken string) (string, string, error) { + s.lock.Lock() + defer s.lock.Unlock() + refreshToken, ok := s.refreshTokens[currentRefreshToken] + if !ok { + return "", "", fmt.Errorf("invalid refresh token") + } + // deletes the refresh token and all access tokens which were issued based on this refresh token + delete(s.refreshTokens, currentRefreshToken) + for _, token := range s.tokens { + if token.RefreshTokenID == currentRefreshToken { + delete(s.tokens, token.ID) + break + } + } + // creates a new refresh token based on the current one + token := uuid.NewString() + refreshToken.Token = token + s.refreshTokens[token] = refreshToken + return token, refreshToken.ID, nil +} + +// accessToken will store an access_token in-memory based on the provided information +func (s *Storage) accessToken(applicationID, refreshTokenID, subject string, audience, scopes []string) (*Token, error) { + s.lock.Lock() + defer s.lock.Unlock() + token := &Token{ + ID: uuid.NewString(), + ApplicationID: applicationID, + RefreshTokenID: refreshTokenID, + Subject: subject, + Audience: audience, + Expiration: time.Now().Add(5 * time.Minute), + Scopes: scopes, + } + s.tokens[token.ID] = token + return token, nil +} + +// setUserinfo sets the info based on the user, scopes and if necessary the clientID +func (s *Storage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSetter, userID, clientID string, scopes []string) (err error) { + s.lock.Lock() + defer s.lock.Unlock() + user := s.userStore.GetUserByID(userID) + if user == nil { + return fmt.Errorf("user not found") + } + for _, scope := range scopes { + switch scope { + case oidc.ScopeOpenID: + userInfo.SetSubject(user.ID) + case oidc.ScopeEmail: + userInfo.SetEmail(user.Email, user.EmailVerified) + case oidc.ScopeProfile: + userInfo.SetPreferredUsername(user.Username) + userInfo.SetName(user.FirstName + " " + user.LastName) + userInfo.SetFamilyName(user.LastName) + userInfo.SetGivenName(user.FirstName) + userInfo.SetLocale(user.PreferredLanguage) + case oidc.ScopePhone: + userInfo.SetPhone(user.Phone, user.PhoneVerified) + case CustomScope: + // you can also have a custom scope and assert public or custom claims based on that + userInfo.AppendClaims(CustomClaim, customClaim(clientID)) + } + } + return nil +} + +// getInfoFromRequest returns the clientID, authTime and amr depending on the op.TokenRequest type / implementation +func getInfoFromRequest(req op.TokenRequest) (clientID string, authTime time.Time, amr []string) { + authReq, ok := req.(*AuthRequest) // Code Flow (with scope offline_access) + if ok { + return authReq.ApplicationID, authReq.authTime, authReq.GetAMR() + } + refreshReq, ok := req.(*RefreshTokenRequest) // Refresh Token Request + if ok { + return refreshReq.ApplicationID, refreshReq.AuthTime, refreshReq.AMR + } + return "", time.Time{}, nil +} + +// customClaim demonstrates how to return custom claims based on provided information +func customClaim(clientID string) map[string]interface{} { + return map[string]interface{}{ + "client": clientID, + "other": "stuff", + } +} + +func appendClaim(claims map[string]interface{}, claim string, value interface{}) map[string]interface{} { + if claims == nil { + claims = make(map[string]interface{}) + } + claims[claim] = value + return claims +} diff --git a/example/server/internal/token.go b/example/server/storage/token.go similarity index 96% rename from example/server/internal/token.go rename to example/server/storage/token.go index 09e675a..ad907e3 100644 --- a/example/server/internal/token.go +++ b/example/server/storage/token.go @@ -1,4 +1,4 @@ -package internal +package storage import "time" diff --git a/example/server/storage/user.go b/example/server/storage/user.go new file mode 100644 index 0000000..423af59 --- /dev/null +++ b/example/server/storage/user.go @@ -0,0 +1,71 @@ +package storage + +import ( + "crypto/rsa" + + "golang.org/x/text/language" +) + +type User struct { + ID string + Username string + Password string + FirstName string + LastName string + Email string + EmailVerified bool + Phone string + PhoneVerified bool + PreferredLanguage language.Tag +} + +type Service struct { + keys map[string]*rsa.PublicKey +} + +type UserStore interface { + GetUserByID(string) *User + GetUserByUsername(string) *User + ExampleClientID() string +} + +type userStore struct { + users map[string]*User +} + +func NewUserStore() UserStore { + return userStore{ + users: map[string]*User{ + "id1": { + ID: "id1", + Username: "test-user", + Password: "verysecure", + FirstName: "Test", + LastName: "User", + Email: "test-user@zitadel.ch", + EmailVerified: true, + Phone: "", + PhoneVerified: false, + PreferredLanguage: language.German, + }, + }, + } +} + +// ExampleClientID is only used in the example server +func (u userStore) ExampleClientID() string { + return "service" +} + +func (u userStore) GetUserByID(id string) *User { + return u.users[id] +} + +func (u userStore) GetUserByUsername(username string) *User { + for _, user := range u.users { + if user.Username == username { + return user + } + } + return nil +} From c4b7ef9160ff277407baee33b3bd678842631733 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Mon, 3 Oct 2022 22:23:59 -0700 Subject: [PATCH 136/502] fix: avoid potential race conditions (#220) * fix potential race condition during signer update * avoid potential race conditions with lazy-initializers in OpenIDProvider * avoid potential race lazy initializers in RelyingParty * review feedback -- additional potential races * add pre-calls to NewRelyingPartyOIDC too --- pkg/client/rp/relying_party.go | 8 ++++++++ pkg/op/op.go | 6 ++++++ pkg/op/signer.go | 6 ++++++ 3 files changed, 20 insertions(+) diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index af202a3..bf0619a 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -154,6 +154,10 @@ func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingPart } } + // avoid races by calling these early + _ = rp.IDTokenVerifier() // sets idTokenVerifier + _ = rp.ErrorHandler() // sets errorHandler + return rp, nil } @@ -186,6 +190,10 @@ func NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, sco rp.oauthConfig.Endpoint = endpoints.Endpoint rp.endpoints = endpoints + // avoid races by calling these early + _ = rp.IDTokenVerifier() // sets idTokenVerifier + _ = rp.ErrorHandler() // sets errorHandler + return rp, nil } diff --git a/pkg/op/op.go b/pkg/op/op.go index bea8569..659464f 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -167,6 +167,12 @@ func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opO o.crypto = NewAESCrypto(config.CryptoKey) + // Avoid potential race conditions by calling these early + _ = o.AccessTokenVerifier() // sets accessTokenVerifier + _ = o.IDTokenHintVerifier() // sets idTokenHintVerifier + _ = o.JWTProfileVerifier() // sets jwtProfileVerifier + _ = o.openIDKeySet() // sets keySet + return o, nil } diff --git a/pkg/op/signer.go b/pkg/op/signer.go index d05bbe5..828876e 100644 --- a/pkg/op/signer.go +++ b/pkg/op/signer.go @@ -3,6 +3,7 @@ package op import ( "context" "errors" + "sync" "github.com/zitadel/logging" "gopkg.in/square/go-jose.v2" @@ -18,6 +19,7 @@ type tokenSigner struct { signer jose.Signer storage AuthStorage alg jose.SignatureAlgorithm + lock sync.RWMutex } func NewSigner(ctx context.Context, storage AuthStorage, keyCh <-chan jose.SigningKey) Signer { @@ -47,6 +49,8 @@ func (s *tokenSigner) Health(_ context.Context) error { } func (s *tokenSigner) Signer() jose.Signer { + s.lock.RLock() + defer s.lock.RUnlock() return s.signer } @@ -62,6 +66,8 @@ func (s *tokenSigner) refreshSigningKey(ctx context.Context, keyCh <-chan jose.S } func (s *tokenSigner) exchangeSigningKey(key jose.SigningKey) { + s.lock.Lock() + defer s.lock.Unlock() s.alg = key.Algorithm if key.Algorithm == "" || key.Key == nil { s.signer = nil From b5da6ec29b7e601ece7c97b8c1f4334cba8acd2b Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Wed, 5 Oct 2022 00:33:10 -0700 Subject: [PATCH 137/502] chore(linting): apply gofumpt & goimports to all .go files (#225) --- example/client/api/api.go | 14 +- example/client/app/app.go | 30 ++--- example/client/service/service.go | 5 +- pkg/client/client.go | 21 ++- pkg/client/key.go | 6 +- pkg/client/profile/jwt_profile.go | 6 +- pkg/client/rp/delegation.go | 4 +- pkg/client/rp/jwks.go | 28 ++-- pkg/client/rp/relying_party.go | 96 +++++++------- pkg/client/rp/tockenexchange.go | 12 +- pkg/client/rp/verifier.go | 26 ++-- pkg/client/rs/resource_server.go | 5 +- pkg/crypto/crypto.go | 4 +- pkg/crypto/hash.go | 4 +- pkg/http/http.go | 9 +- pkg/oidc/authorization.go | 56 ++++---- pkg/oidc/discovery.go | 102 +++++++------- pkg/oidc/grants/client_credentials.go | 8 +- pkg/oidc/jwt_profile.go | 4 +- pkg/oidc/keyset.go | 34 ++--- pkg/oidc/session.go | 2 +- pkg/oidc/token.go | 64 ++++----- pkg/oidc/token_request.go | 49 +++---- pkg/oidc/verifier.go | 11 +- pkg/op/auth_request.go | 58 ++++---- pkg/op/auth_request_test.go | 184 +++++++++++++++++--------- pkg/op/config_test.go | 2 +- pkg/op/discovery.go | 8 +- pkg/op/endpoint.go | 2 +- pkg/op/endpoint_test.go | 2 +- pkg/op/mock/authorizer.mock.impl.go | 1 + pkg/op/mock/client.go | 3 +- pkg/op/mock/storage.mock.impl.go | 11 ++ pkg/op/op.go | 38 +++--- pkg/op/probes.go | 1 + pkg/op/token_client_credentials.go | 10 +- pkg/op/token_code.go | 16 +-- pkg/op/token_exchange.go | 2 +- pkg/op/token_jwt_profile.go | 6 +- pkg/op/token_refresh.go | 24 ++-- pkg/op/token_request.go | 22 +-- pkg/op/token_revocation.go | 6 +- pkg/op/verifier_access_token.go | 12 +- pkg/op/verifier_id_token_hint.go | 2 +- pkg/op/verifier_jwt_profile.go | 8 +- 45 files changed, 539 insertions(+), 479 deletions(-) diff --git a/example/client/api/api.go b/example/client/api/api.go index 4fe09ed..2220554 100644 --- a/example/client/api/api.go +++ b/example/client/api/api.go @@ -34,14 +34,14 @@ func main() { router := mux.NewRouter() - //public url accessible without any authorization - //will print `OK` and current timestamp + // public url accessible without any authorization + // will print `OK` and current timestamp router.HandleFunc(publicURL, func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK " + time.Now().String())) }) - //protected url which needs an active token - //will print the result of the introspection endpoint on success + // protected url which needs an active token + // will print the result of the introspection endpoint on success router.HandleFunc(protectedURL, func(w http.ResponseWriter, r *http.Request) { ok, token := checkToken(w, r) if !ok { @@ -60,9 +60,9 @@ func main() { w.Write(data) }) - //protected url which needs an active token and checks if the response of the introspect endpoint - //contains a requested claim with the required (string) value - //e.g. /protected/username/livio@caos.ch + // protected url which needs an active token and checks if the response of the introspect endpoint + // contains a requested claim with the required (string) value + // e.g. /protected/username/livio@caos.ch router.HandleFunc(protectedClaimURL, func(w http.ResponseWriter, r *http.Request) { ok, token := checkToken(w, r) if !ok { diff --git a/example/client/app/app.go b/example/client/app/app.go index 10453b1..c2c8c3f 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -48,18 +48,18 @@ func main() { logrus.Fatalf("error creating provider %s", err.Error()) } - //generate some state (representing the state of the user in your application, - //e.g. the page where he was before sending him to login + // generate some state (representing the state of the user in your application, + // e.g. the page where he was before sending him to login state := func() string { return uuid.New().String() } - //register the AuthURLHandler at your preferred path - //the AuthURLHandler creates the auth request and redirects the user to the auth server - //including state handling with secure cookie and the possibility to use PKCE + // register the AuthURLHandler at your preferred path + // the AuthURLHandler creates the auth request and redirects the user to the auth server + // including state handling with secure cookie and the possibility to use PKCE http.Handle("/login", rp.AuthURLHandler(state, provider)) - //for demonstration purposes the returned userinfo response is written as JSON object onto response + // for demonstration purposes the returned userinfo response is written as JSON object onto response marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) { data, err := json.Marshal(info) if err != nil { @@ -69,9 +69,9 @@ func main() { w.Write(data) } - //you could also just take the access_token and id_token without calling the userinfo endpoint: + // you could also just take the access_token and id_token without calling the userinfo endpoint: // - //marshalToken := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty) { + // marshalToken := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty) { // data, err := json.Marshal(tokens) // if err != nil { // http.Error(w, err.Error(), http.StatusInternalServerError) @@ -80,16 +80,16 @@ func main() { // w.Write(data) //} - //register the CodeExchangeHandler at the callbackPath - //the CodeExchangeHandler handles the auth response, creates the token request and calls the callback function - //with the returned tokens from the token endpoint - //in this example the callback function itself is wrapped by the UserinfoCallback which - //will call the Userinfo endpoint, check the sub and pass the info into the callback function + // register the CodeExchangeHandler at the callbackPath + // the CodeExchangeHandler handles the auth response, creates the token request and calls the callback function + // with the returned tokens from the token endpoint + // in this example the callback function itself is wrapped by the UserinfoCallback which + // will call the Userinfo endpoint, check the sub and pass the info into the callback function http.Handle(callbackPath, rp.CodeExchangeHandler(rp.UserinfoCallback(marshalUserinfo), provider)) - //if you would use the callback without calling the userinfo endpoint, simply switch the callback handler for: + // if you would use the callback without calling the userinfo endpoint, simply switch the callback handler for: // - //http.Handle(callbackPath, rp.CodeExchangeHandler(marshalToken, provider)) + // http.Handle(callbackPath, rp.CodeExchangeHandler(marshalToken, provider)) lis := fmt.Sprintf("127.0.0.1:%s", port) logrus.Infof("listening on http://%s/", lis) diff --git a/example/client/service/service.go b/example/client/service/service.go index f406c6d..980abcd 100644 --- a/example/client/service/service.go +++ b/example/client/service/service.go @@ -16,9 +16,7 @@ import ( "github.com/zitadel/oidc/pkg/client/profile" ) -var ( - client = http.DefaultClient -) +var client = http.DefaultClient func main() { keyPath := os.Getenv("KEY_PATH") @@ -145,7 +143,6 @@ func main() { if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } - }) lis := fmt.Sprintf("127.0.0.1:%s", port) logrus.Infof("listening on http://%s/", lis) diff --git a/pkg/client/client.go b/pkg/client/client.go index ccc3cc0..d6d27f7 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -15,20 +15,17 @@ import ( "github.com/zitadel/oidc/pkg/oidc" ) -var ( - Encoder = func() httphelper.Encoder { - e := schema.NewEncoder() - e.RegisterEncoder(oidc.SpaceDelimitedArray{}, func(value reflect.Value) string { - return value.Interface().(oidc.SpaceDelimitedArray).Encode() - }) - return e - }() -) +var Encoder = func() httphelper.Encoder { + e := schema.NewEncoder() + e.RegisterEncoder(oidc.SpaceDelimitedArray{}, func(value reflect.Value) string { + return value.Interface().(oidc.SpaceDelimitedArray).Encode() + }) + return e +}() -//Discover calls the discovery endpoint of the provided issuer and returns its configuration -//It accepts an optional argument "wellknownUrl" which can be used to overide the dicovery endpoint url +// Discover calls the discovery endpoint of the provided issuer and returns its configuration +// It accepts an optional argument "wellknownUrl" which can be used to overide the dicovery endpoint url func Discover(issuer string, httpClient *http.Client, wellKnownUrl ...string) (*oidc.DiscoveryConfiguration, error) { - wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint if len(wellKnownUrl) == 1 && wellKnownUrl[0] != "" { wellKnown = wellKnownUrl[0] diff --git a/pkg/client/key.go b/pkg/client/key.go index f89a2b4..740c6d3 100644 --- a/pkg/client/key.go +++ b/pkg/client/key.go @@ -14,12 +14,12 @@ type keyFile struct { Type string `json:"type"` // serviceaccount or application KeyID string `json:"keyId"` Key string `json:"key"` - Issuer string `json:"issuer"` //not yet in file + Issuer string `json:"issuer"` // not yet in file - //serviceaccount + // serviceaccount UserID string `json:"userId"` - //application + // application ClientID string `json:"clientId"` } diff --git a/pkg/client/profile/jwt_profile.go b/pkg/client/profile/jwt_profile.go index 03fa52a..b29fcaa 100644 --- a/pkg/client/profile/jwt_profile.go +++ b/pkg/client/profile/jwt_profile.go @@ -11,9 +11,9 @@ import ( "github.com/zitadel/oidc/pkg/oidc" ) -//jwtProfileTokenSource implement the oauth2.TokenSource -//it will request a token using the OAuth2 JWT Profile Grant -//therefore sending an `assertion` by singing a JWT with the provided private key +// jwtProfileTokenSource implement the oauth2.TokenSource +// it will request a token using the OAuth2 JWT Profile Grant +// therefore sending an `assertion` by singing a JWT with the provided private key type jwtProfileTokenSource struct { clientID string audience []string diff --git a/pkg/client/rp/delegation.go b/pkg/client/rp/delegation.go index 463a5c4..a2b1f00 100644 --- a/pkg/client/rp/delegation.go +++ b/pkg/client/rp/delegation.go @@ -4,8 +4,8 @@ import ( "github.com/zitadel/oidc/pkg/oidc/grants/tokenexchange" ) -//DelegationTokenRequest is an implementation of TokenExchangeRequest -//it exchanges an "urn:ietf:params:oauth:token-type:access_token" with an optional +// DelegationTokenRequest is an implementation of TokenExchangeRequest +// it exchanges an "urn:ietf:params:oauth:token-type:access_token" with an optional //"urn:ietf:params:oauth:token-type:access_token" actor token for an //"urn:ietf:params:oauth:token-type:access_token" delegation token func DelegationTokenRequest(subjectToken string, opts ...tokenexchange.TokenExchangeOption) *tokenexchange.TokenExchangeRequest { diff --git a/pkg/client/rp/jwks.go b/pkg/client/rp/jwks.go index 0624d6b..cc49eb7 100644 --- a/pkg/client/rp/jwks.go +++ b/pkg/client/rp/jwks.go @@ -21,12 +21,12 @@ func NewRemoteKeySet(client *http.Client, jwksURL string, opts ...func(*remoteKe return keyset } -//SkipRemoteCheck will suppress checking for new remote keys if signature validation fails with cached keys -//and no kid header is set in the JWT +// SkipRemoteCheck will suppress checking for new remote keys if signature validation fails with cached keys +// and no kid header is set in the JWT // -//this might be handy to save some unnecessary round trips in cases where the JWT does not contain a kid header and -//there is only a single remote key -//please notice that remote keys will then only be fetched if cached keys are empty +// this might be handy to save some unnecessary round trips in cases where the JWT does not contain a kid header and +// there is only a single remote key +// please notice that remote keys will then only be fetched if cached keys are empty func SkipRemoteCheck() func(set *remoteKeySet) { return func(set *remoteKeySet) { set.skipRemoteCheck = true @@ -97,15 +97,15 @@ func (r *remoteKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSig return r.verifySignatureRemote(ctx, jws, keyID, alg) } -//verifySignatureCached checks for a matching key in the cached key list +// verifySignatureCached checks for a matching key in the cached key list // -//if there is only one possible, it tries to verify the signature and will return the payload if successful +// if there is only one possible, it tries to verify the signature and will return the payload if successful // -//it only returns an error if signature validation fails and keys exactMatch which is if either: +// it only returns an error if signature validation fails and keys exactMatch which is if either: // - both kid are empty and skipRemoteCheck is set to true // - or both (JWT and JWK) kid are equal // -//otherwise it will return no error (so remote keys will be loaded) +// otherwise it will return no error (so remote keys will be loaded) func (r *remoteKeySet) verifySignatureCached(jws *jose.JSONWebSignature, keyID, alg string) ([]byte, error) { keys := r.keysFromCache() if len(keys) == 0 { @@ -113,7 +113,7 @@ func (r *remoteKeySet) verifySignatureCached(jws *jose.JSONWebSignature, keyID, } key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, keys...) if err != nil { - //no key / multiple found, try with remote keys + // no key / multiple found, try with remote keys return nil, nil //nolint:nilerr } payload, err := jws.Verify(&key) @@ -121,7 +121,7 @@ func (r *remoteKeySet) verifySignatureCached(jws *jose.JSONWebSignature, keyID, return payload, nil } if !r.exactMatch(key.KeyID, keyID) { - //no exact key match, try getting better match with remote keys + // no exact key match, try getting better match with remote keys return nil, nil } return nil, fmt.Errorf("signature verification failed: %w", err) @@ -213,11 +213,11 @@ func (r *remoteKeySet) fetchRemoteKeys(ctx context.Context) ([]jose.JSONWebKey, return keySet.Keys, nil } -//jsonWebKeySet is an alias for jose.JSONWebKeySet which ignores unknown key types (kty) +// jsonWebKeySet is an alias for jose.JSONWebKeySet which ignores unknown key types (kty) type jsonWebKeySet jose.JSONWebKeySet -//UnmarshalJSON overrides the default jose.JSONWebKeySet method to ignore any error -//which might occur because of unknown key types (kty) +// UnmarshalJSON overrides the default jose.JSONWebKeySet method to ignore any error +// which might occur because of unknown key types (kty) func (k *jsonWebKeySet) UnmarshalJSON(data []byte) (err error) { var raw rawJSONWebKeySet err = json.Unmarshal(data, &raw) diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index bf0619a..cb271e7 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -23,53 +23,49 @@ const ( pkceCode = "pkce" ) -var ( - ErrUserInfoSubNotMatching = errors.New("sub from userinfo does not match the sub from the id_token") -) +var ErrUserInfoSubNotMatching = errors.New("sub from userinfo does not match the sub from the id_token") -//RelyingParty declares the minimal interface for oidc clients +// RelyingParty declares the minimal interface for oidc clients type RelyingParty interface { - //OAuthConfig returns the oauth2 Config + // OAuthConfig returns the oauth2 Config OAuthConfig() *oauth2.Config - //Issuer returns the issuer of the oidc config + // Issuer returns the issuer of the oidc config Issuer() string - //IsPKCE returns if authorization is done using `Authorization Code Flow with Proof Key for Code Exchange (PKCE)` + // IsPKCE returns if authorization is done using `Authorization Code Flow with Proof Key for Code Exchange (PKCE)` IsPKCE() bool - //CookieHandler returns a http cookie handler used for various state transfer cookies + // CookieHandler returns a http cookie handler used for various state transfer cookies CookieHandler() *httphelper.CookieHandler - //HttpClient returns a http client used for calls to the openid provider, e.g. calling token endpoint + // HttpClient returns a http client used for calls to the openid provider, e.g. calling token endpoint HttpClient() *http.Client - //IsOAuth2Only specifies whether relaying party handles only oauth2 or oidc calls + // IsOAuth2Only specifies whether relaying party handles only oauth2 or oidc calls IsOAuth2Only() bool - //Signer is used if the relaying party uses the JWT Profile + // Signer is used if the relaying party uses the JWT Profile Signer() jose.Signer - //GetEndSessionEndpoint returns the endpoint to sign out on a IDP + // GetEndSessionEndpoint returns the endpoint to sign out on a IDP GetEndSessionEndpoint() string - //UserinfoEndpoint returns the userinfo + // UserinfoEndpoint returns the userinfo UserinfoEndpoint() string - //IDTokenVerifier returns the verifier interface used for oidc id_token verification + // IDTokenVerifier returns the verifier interface used for oidc id_token verification IDTokenVerifier() IDTokenVerifier - //ErrorHandler returns the handler used for callback errors + // ErrorHandler returns the handler used for callback errors ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) } type ErrorHandler func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) -var ( - DefaultErrorHandler ErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) { - http.Error(w, errorType+": "+errorDesc, http.StatusInternalServerError) - } -) +var DefaultErrorHandler ErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) { + http.Error(w, errorType+": "+errorDesc, http.StatusInternalServerError) +} type relyingParty struct { issuer string @@ -138,9 +134,9 @@ func (rp *relyingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, return rp.errorHandler } -//NewRelyingPartyOAuth creates an (OAuth2) RelyingParty with the given -//OAuth2 Config and possible configOptions -//it will use the AuthURL and TokenURL set in config +// NewRelyingPartyOAuth creates an (OAuth2) RelyingParty with the given +// OAuth2 Config and possible configOptions +// it will use the AuthURL and TokenURL set in config func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingParty, error) { rp := &relyingParty{ oauthConfig: config, @@ -161,9 +157,9 @@ func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingPart return rp, nil } -//NewRelyingPartyOIDC creates an (OIDC) RelyingParty with the given -//issuer, clientID, clientSecret, redirectURI, scopes and possible configOptions -//it will run discovery on the provided issuer and use the found endpoints +// NewRelyingPartyOIDC creates an (OIDC) RelyingParty with the given +// issuer, clientID, clientSecret, redirectURI, scopes and possible configOptions +// it will run discovery on the provided issuer and use the found endpoints func NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelyingParty, error) { rp := &relyingParty{ issuer: issuer, @@ -197,7 +193,7 @@ func NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, sco return rp, nil } -//Option is the type for providing dynamic options to the relyingParty +// Option is the type for providing dynamic options to the relyingParty type Option func(*relyingParty) error func WithCustomDiscoveryUrl(url string) Option { @@ -207,7 +203,7 @@ func WithCustomDiscoveryUrl(url string) Option { } } -//WithCookieHandler set a `CookieHandler` for securing the various redirects +// WithCookieHandler set a `CookieHandler` for securing the various redirects func WithCookieHandler(cookieHandler *httphelper.CookieHandler) Option { return func(rp *relyingParty) error { rp.cookieHandler = cookieHandler @@ -215,9 +211,9 @@ func WithCookieHandler(cookieHandler *httphelper.CookieHandler) Option { } } -//WithPKCE sets the RP to use PKCE (oauth2 code challenge) -//it also sets a `CookieHandler` for securing the various redirects -//and exchanging the code challenge +// WithPKCE sets the RP to use PKCE (oauth2 code challenge) +// it also sets a `CookieHandler` for securing the various redirects +// and exchanging the code challenge func WithPKCE(cookieHandler *httphelper.CookieHandler) Option { return func(rp *relyingParty) error { rp.pkce = true @@ -226,7 +222,7 @@ func WithPKCE(cookieHandler *httphelper.CookieHandler) Option { } } -//WithHTTPClient provides the ability to set an http client to be used for the relaying party and verifier +// WithHTTPClient provides the ability to set an http client to be used for the relaying party and verifier func WithHTTPClient(client *http.Client) Option { return func(rp *relyingParty) error { rp.httpClient = client @@ -297,7 +293,7 @@ func SignerFromKeyAndKeyID(key []byte, keyID string) SignerFromKey { } } -//Discover calls the discovery endpoint of the provided issuer and returns the found endpoints +// Discover calls the discovery endpoint of the provided issuer and returns the found endpoints // //deprecated: use client.Discover func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { @@ -317,7 +313,7 @@ func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { return GetEndpoints(discoveryConfig), nil } -//AuthURL returns the auth request url +// AuthURL returns the auth request url //(wrapping the oauth2 `AuthCodeURL`) func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string { authOpts := make([]oauth2.AuthCodeOption, 0) @@ -327,8 +323,8 @@ func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string { return rp.OAuthConfig().AuthCodeURL(state, authOpts...) } -//AuthURLHandler extends the `AuthURL` method with a http redirect handler -//including handling setting cookie for secure `state` transfer +// AuthURLHandler extends the `AuthURL` method with a http redirect handler +// including handling setting cookie for secure `state` transfer func AuthURLHandler(stateFn func() string, rp RelyingParty) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { opts := make([]AuthURLOpt, 0) @@ -349,7 +345,7 @@ func AuthURLHandler(stateFn func() string, rp RelyingParty) http.HandlerFunc { } } -//GenerateAndStoreCodeChallenge generates a PKCE code challenge and stores its verifier into a secure cookie +// GenerateAndStoreCodeChallenge generates a PKCE code challenge and stores its verifier into a secure cookie func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (string, error) { codeVerifier := base64.RawURLEncoding.EncodeToString([]byte(uuid.New().String())) if err := rp.CookieHandler().SetCookie(w, pkceCode, codeVerifier); err != nil { @@ -358,8 +354,8 @@ func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (stri return oidc.NewSHACodeChallenge(codeVerifier), nil } -//CodeExchange handles the oauth2 code exchange, extracting and validating the id_token -//returning it parsed together with the oauth2 tokens (access, refresh) +// CodeExchange handles the oauth2 code exchange, extracting and validating the id_token +// returning it parsed together with the oauth2 tokens (access, refresh) func CodeExchange(ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) { ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient()) codeOpts := make([]oauth2.AuthCodeOption, 0) @@ -391,9 +387,9 @@ func CodeExchange(ctx context.Context, code string, rp RelyingParty, opts ...Cod type CodeExchangeCallback func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp RelyingParty) -//CodeExchangeHandler extends the `CodeExchange` method with a http handler -//including cookie handling for secure `state` transfer -//and optional PKCE code verifier checking +// CodeExchangeHandler extends the `CodeExchange` method with a http handler +// including cookie handling for secure `state` transfer +// and optional PKCE code verifier checking func CodeExchangeHandler(callback CodeExchangeCallback, rp RelyingParty) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { state, err := tryReadStateCookie(w, r, rp) @@ -434,9 +430,9 @@ func CodeExchangeHandler(callback CodeExchangeCallback, rp RelyingParty) http.Ha type CodeExchangeUserinfoCallback func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, provider RelyingParty, info oidc.UserInfo) -//UserinfoCallback wraps the callback function of the CodeExchangeHandler -//and calls the userinfo endpoint with the access token -//on success it will pass the userinfo into its callback function as well +// UserinfoCallback wraps the callback function of the CodeExchangeHandler +// and calls the userinfo endpoint with the access token +// on success it will pass the userinfo into its callback function as well func UserinfoCallback(f CodeExchangeUserinfoCallback) CodeExchangeCallback { return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp RelyingParty) { info, err := Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp) @@ -448,7 +444,7 @@ func UserinfoCallback(f CodeExchangeUserinfoCallback) CodeExchangeCallback { } } -//Userinfo will call the OIDC Userinfo Endpoint with the provided token +// Userinfo will call the OIDC Userinfo Endpoint with the provided token func Userinfo(token, tokenType, subject string, rp RelyingParty) (oidc.UserInfo, error) { req, err := http.NewRequest("GET", rp.UserinfoEndpoint(), nil) if err != nil { @@ -512,7 +508,7 @@ func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints { type AuthURLOpt func() []oauth2.AuthCodeOption -//WithCodeChallenge sets the `code_challenge` params in the auth request +// WithCodeChallenge sets the `code_challenge` params in the auth request func WithCodeChallenge(codeChallenge string) AuthURLOpt { return func() []oauth2.AuthCodeOption { return []oauth2.AuthCodeOption{ @@ -522,7 +518,7 @@ func WithCodeChallenge(codeChallenge string) AuthURLOpt { } } -//WithPrompt sets the `prompt` params in the auth request +// WithPrompt sets the `prompt` params in the auth request func WithPrompt(prompt ...string) AuthURLOpt { return func() []oauth2.AuthCodeOption { return []oauth2.AuthCodeOption{ @@ -533,14 +529,14 @@ func WithPrompt(prompt ...string) AuthURLOpt { type CodeExchangeOpt func() []oauth2.AuthCodeOption -//WithCodeVerifier sets the `code_verifier` param in the token request +// WithCodeVerifier sets the `code_verifier` param in the token request func WithCodeVerifier(codeVerifier string) CodeExchangeOpt { return func() []oauth2.AuthCodeOption { return []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("code_verifier", codeVerifier)} } } -//WithClientAssertionJWT sets the `client_assertion` param in the token request +// WithClientAssertionJWT sets the `client_assertion` param in the token request func WithClientAssertionJWT(clientAssertion string) CodeExchangeOpt { return func() []oauth2.AuthCodeOption { return client.ClientAssertionCodeOptions(clientAssertion) diff --git a/pkg/client/rp/tockenexchange.go b/pkg/client/rp/tockenexchange.go index a1cb3be..3950fe1 100644 --- a/pkg/client/rp/tockenexchange.go +++ b/pkg/client/rp/tockenexchange.go @@ -8,20 +8,20 @@ import ( "github.com/zitadel/oidc/pkg/oidc/grants/tokenexchange" ) -//TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange` +// TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange` type TokenExchangeRP interface { RelyingParty - //TokenExchange implement the `Token Exchange Grant` exchanging some token for an other + // TokenExchange implement the `Token Exchange Grant` exchanging some token for an other TokenExchange(context.Context, *tokenexchange.TokenExchangeRequest) (*oauth2.Token, error) } -//DelegationTokenExchangeRP extends the `TokenExchangeRP` interface -//for the specific `delegation token` request +// DelegationTokenExchangeRP extends the `TokenExchangeRP` interface +// for the specific `delegation token` request type DelegationTokenExchangeRP interface { TokenExchangeRP - //DelegationTokenExchange implement the `Token Exchange Grant` - //providing an access token in request for a `delegation` token for a given resource / audience + // DelegationTokenExchange implement the `Token Exchange Grant` + // providing an access token in request for a `delegation` token for a given resource / audience DelegationTokenExchange(context.Context, string, ...tokenexchange.TokenExchangeOption) (*oauth2.Token, error) } diff --git a/pkg/client/rp/verifier.go b/pkg/client/rp/verifier.go index 6cf5614..6b3b3fd 100644 --- a/pkg/client/rp/verifier.go +++ b/pkg/client/rp/verifier.go @@ -19,7 +19,7 @@ type IDTokenVerifier interface { MaxAge() time.Duration } -//VerifyTokens implement the Token Response Validation as defined in OIDC specification +// VerifyTokens implement the Token Response Validation as defined in OIDC specification //https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation func VerifyTokens(ctx context.Context, accessToken, idTokenString string, v IDTokenVerifier) (oidc.IDTokenClaims, error) { idToken, err := VerifyIDToken(ctx, idTokenString, v) @@ -32,7 +32,7 @@ func VerifyTokens(ctx context.Context, accessToken, idTokenString string, v IDTo return idToken, nil } -//VerifyIDToken validates the id token according to +// VerifyIDToken validates the id token according to //https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation func VerifyIDToken(ctx context.Context, token string, v IDTokenVerifier) (oidc.IDTokenClaims, error) { claims := oidc.EmptyIDTokenClaims() @@ -88,7 +88,7 @@ func VerifyIDToken(ctx context.Context, token string, v IDTokenVerifier) (oidc.I return claims, nil } -//VerifyAccessToken validates the access token according to +// VerifyAccessToken validates the access token according to //https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAlgorithm) error { if atHash == "" { @@ -105,8 +105,8 @@ func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAl return nil } -//NewIDTokenVerifier returns an implementation of `IDTokenVerifier` -//for `VerifyTokens` and `VerifyIDToken` +// NewIDTokenVerifier returns an implementation of `IDTokenVerifier` +// for `VerifyTokens` and `VerifyIDToken` func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ...VerifierOption) IDTokenVerifier { v := &idTokenVerifier{ issuer: issuer, @@ -125,46 +125,46 @@ func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ... return v } -//VerifierOption is the type for providing dynamic options to the IDTokenVerifier +// VerifierOption is the type for providing dynamic options to the IDTokenVerifier type VerifierOption func(*idTokenVerifier) -//WithIssuedAtOffset mitigates the risk of iat to be in the future -//because of clock skews with the ability to add an offset to the current time +// WithIssuedAtOffset mitigates the risk of iat to be in the future +// because of clock skews with the ability to add an offset to the current time func WithIssuedAtOffset(offset time.Duration) func(*idTokenVerifier) { return func(v *idTokenVerifier) { v.offset = offset } } -//WithIssuedAtMaxAge provides the ability to define the maximum duration between iat and now +// WithIssuedAtMaxAge provides the ability to define the maximum duration between iat and now func WithIssuedAtMaxAge(maxAge time.Duration) func(*idTokenVerifier) { return func(v *idTokenVerifier) { v.maxAge = maxAge } } -//WithNonce sets the function to check the nonce +// WithNonce sets the function to check the nonce func WithNonce(nonce func(context.Context) string) VerifierOption { return func(v *idTokenVerifier) { v.nonce = nonce } } -//WithACRVerifier sets the verifier for the acr claim +// WithACRVerifier sets the verifier for the acr claim func WithACRVerifier(verifier oidc.ACRVerifier) VerifierOption { return func(v *idTokenVerifier) { v.acr = verifier } } -//WithAuthTimeMaxAge provides the ability to define the maximum duration between auth_time and now +// WithAuthTimeMaxAge provides the ability to define the maximum duration between auth_time and now func WithAuthTimeMaxAge(maxAge time.Duration) VerifierOption { return func(v *idTokenVerifier) { v.maxAge = maxAge } } -//WithSupportedSigningAlgorithms overwrites the default RS256 signing algorithm +// WithSupportedSigningAlgorithms overwrites the default RS256 signing algorithm func WithSupportedSigningAlgorithms(algs ...string) VerifierOption { return func(v *idTokenVerifier) { v.supportedSignAlgs = algs diff --git a/pkg/client/rs/resource_server.go b/pkg/client/rs/resource_server.go index 63b29d4..b1bc47e 100644 --- a/pkg/client/rs/resource_server.go +++ b/pkg/client/rs/resource_server.go @@ -43,6 +43,7 @@ func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, o } return newResourceServer(issuer, authorizer, option...) } + func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...Option) (ResourceServer, error) { signer, err := client.NewSignerFromPrivateKeyByte(key, keyID) if err != nil { @@ -91,14 +92,14 @@ func NewResourceServerFromKeyFile(issuer, path string, options ...Option) (Resou type Option func(*resourceServer) -//WithClient provides the ability to set an http client to be used for the resource server +// WithClient provides the ability to set an http client to be used for the resource server func WithClient(client *http.Client) Option { return func(server *resourceServer) { server.httpClient = client } } -//WithStaticEndpoints provides the ability to set static token and introspect URL +// WithStaticEndpoints provides the ability to set static token and introspect URL func WithStaticEndpoints(tokenURL, introspectURL string) Option { return func(server *resourceServer) { server.tokenURL = tokenURL diff --git a/pkg/crypto/crypto.go b/pkg/crypto/crypto.go index 488d8a4..109fa0b 100644 --- a/pkg/crypto/crypto.go +++ b/pkg/crypto/crypto.go @@ -9,9 +9,7 @@ import ( "io" ) -var ( - ErrCipherTextBlockSize = errors.New("ciphertext block size is too short") -) +var ErrCipherTextBlockSize = errors.New("ciphertext block size is too short") func EncryptAES(data string, key string) (string, error) { encrypted, err := EncryptBytesAES([]byte(data), key) diff --git a/pkg/crypto/hash.go b/pkg/crypto/hash.go index 6529249..6fcc71f 100644 --- a/pkg/crypto/hash.go +++ b/pkg/crypto/hash.go @@ -11,9 +11,7 @@ import ( "gopkg.in/square/go-jose.v2" ) -var ( - ErrUnsupportedAlgorithm = errors.New("unsupported signing algorithm") -) +var ErrUnsupportedAlgorithm = errors.New("unsupported signing algorithm") func GetHashAlgorithm(sigAlgorithm jose.SignatureAlgorithm) (hash.Hash, error) { switch sigAlgorithm { diff --git a/pkg/http/http.go b/pkg/http/http.go index b0d1ef7..0e97b6f 100644 --- a/pkg/http/http.go +++ b/pkg/http/http.go @@ -12,15 +12,14 @@ import ( "time" ) -var ( - DefaultHTTPClient = &http.Client{ - Timeout: 30 * time.Second, - } -) +var DefaultHTTPClient = &http.Client{ + Timeout: 30 * time.Second, +} type Decoder interface { Decode(dst interface{}, src map[string][]string) error } + type Encoder interface { Encode(src interface{}, dst map[string][]string) error } diff --git a/pkg/oidc/authorization.go b/pkg/oidc/authorization.go index e6cfe58..f620ecb 100644 --- a/pkg/oidc/authorization.go +++ b/pkg/oidc/authorization.go @@ -1,40 +1,40 @@ package oidc const ( - //ScopeOpenID defines the scope `openid` - //OpenID Connect requests MUST contain the `openid` scope value + // ScopeOpenID defines the scope `openid` + // OpenID Connect requests MUST contain the `openid` scope value ScopeOpenID = "openid" - //ScopeProfile defines the scope `profile` - //This (optional) scope value requests access to the End-User's default profile Claims, - //which are: name, family_name, given_name, middle_name, nickname, preferred_username, - //profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at. + // ScopeProfile defines the scope `profile` + // This (optional) scope value requests access to the End-User's default profile Claims, + // which are: name, family_name, given_name, middle_name, nickname, preferred_username, + // profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at. ScopeProfile = "profile" - //ScopeEmail defines the scope `email` - //This (optional) scope value requests access to the email and email_verified Claims. + // ScopeEmail defines the scope `email` + // This (optional) scope value requests access to the email and email_verified Claims. ScopeEmail = "email" - //ScopeAddress defines the scope `address` - //This (optional) scope value requests access to the address Claim. + // ScopeAddress defines the scope `address` + // This (optional) scope value requests access to the address Claim. ScopeAddress = "address" - //ScopePhone defines the scope `phone` - //This (optional) scope value requests access to the phone_number and phone_number_verified Claims. + // ScopePhone defines the scope `phone` + // This (optional) scope value requests access to the phone_number and phone_number_verified Claims. ScopePhone = "phone" - //ScopeOfflineAccess defines the scope `offline_access` - //This (optional) scope value requests that an OAuth 2.0 Refresh Token be issued that can be used to obtain an Access Token - //that grants access to the End-User's UserInfo Endpoint even when the End-User is not present (not logged in). + // ScopeOfflineAccess defines the scope `offline_access` + // This (optional) scope value requests that an OAuth 2.0 Refresh Token be issued that can be used to obtain an Access Token + // that grants access to the End-User's UserInfo Endpoint even when the End-User is not present (not logged in). ScopeOfflineAccess = "offline_access" - //ResponseTypeCode for the Authorization Code Flow returning a code from the Authorization Server + // ResponseTypeCode for the Authorization Code Flow returning a code from the Authorization Server ResponseTypeCode ResponseType = "code" - //ResponseTypeIDToken for the Implicit Flow returning id and access tokens directly from the Authorization Server + // ResponseTypeIDToken for the Implicit Flow returning id and access tokens directly from the Authorization Server ResponseTypeIDToken ResponseType = "id_token token" - //ResponseTypeIDTokenOnly for the Implicit Flow returning only id token directly from the Authorization Server + // ResponseTypeIDTokenOnly for the Implicit Flow returning only id token directly from the Authorization Server ResponseTypeIDTokenOnly ResponseType = "id_token" DisplayPage Display = "page" @@ -45,21 +45,21 @@ const ( ResponseModeQuery ResponseMode = "query" ResponseModeFragment ResponseMode = "fragment" - //PromptNone (`none`) disallows the Authorization Server to display any authentication or consent user interface pages. - //An error (login_required, interaction_required, ...) will be returned if the user is not already authenticated or consent is needed + // PromptNone (`none`) disallows the Authorization Server to display any authentication or consent user interface pages. + // An error (login_required, interaction_required, ...) will be returned if the user is not already authenticated or consent is needed PromptNone = "none" - //PromptLogin (`login`) directs the Authorization Server to prompt the End-User for reauthentication. + // PromptLogin (`login`) directs the Authorization Server to prompt the End-User for reauthentication. PromptLogin = "login" - //PromptConsent (`consent`) directs the Authorization Server to prompt the End-User for consent (of sharing information). + // PromptConsent (`consent`) directs the Authorization Server to prompt the End-User for consent (of sharing information). PromptConsent = "consent" - //PromptSelectAccount (`select_account `) directs the Authorization Server to prompt the End-User to select a user account (to enable multi user / session switching) + // PromptSelectAccount (`select_account `) directs the Authorization Server to prompt the End-User to select a user account (to enable multi user / session switching) PromptSelectAccount = "select_account" ) -//AuthRequest according to: +// AuthRequest according to: //https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest type AuthRequest struct { Scopes SpaceDelimitedArray `json:"scope" schema:"scope"` @@ -82,21 +82,21 @@ type AuthRequest struct { CodeChallenge string `json:"code_challenge" schema:"code_challenge"` CodeChallengeMethod CodeChallengeMethod `json:"code_challenge_method" schema:"code_challenge_method"` - //RequestParam enables OIDC requests to be passed in a single, self-contained parameter (as JWT, called Request Object) + // RequestParam enables OIDC requests to be passed in a single, self-contained parameter (as JWT, called Request Object) RequestParam string `schema:"request"` } -//GetRedirectURI returns the redirect_uri value for the ErrAuthRequest interface +// GetRedirectURI returns the redirect_uri value for the ErrAuthRequest interface func (a *AuthRequest) GetRedirectURI() string { return a.RedirectURI } -//GetResponseType returns the response_type value for the ErrAuthRequest interface +// GetResponseType returns the response_type value for the ErrAuthRequest interface func (a *AuthRequest) GetResponseType() ResponseType { return a.ResponseType } -//GetState returns the optional state value for the ErrAuthRequest interface +// GetState returns the optional state value for the ErrAuthRequest interface func (a *AuthRequest) GetState() string { return a.State } diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go index 673d628..4a817e8 100644 --- a/pkg/oidc/discovery.go +++ b/pkg/oidc/discovery.go @@ -9,143 +9,143 @@ const ( ) type DiscoveryConfiguration struct { - //Issuer is the identifier of the OP and is used in the tokens as `iss` claim. + // Issuer is the identifier of the OP and is used in the tokens as `iss` claim. Issuer string `json:"issuer,omitempty"` - //AuthorizationEndpoint is the URL of the OAuth 2.0 Authorization Endpoint where all user interactive login start + // AuthorizationEndpoint is the URL of the OAuth 2.0 Authorization Endpoint where all user interactive login start AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` - //TokenEndpoint is the URL of the OAuth 2.0 Token Endpoint where all tokens are issued, except when using Implicit Flow + // TokenEndpoint is the URL of the OAuth 2.0 Token Endpoint where all tokens are issued, except when using Implicit Flow TokenEndpoint string `json:"token_endpoint,omitempty"` - //IntrospectionEndpoint is the URL of the OAuth 2.0 Introspection Endpoint. + // IntrospectionEndpoint is the URL of the OAuth 2.0 Introspection Endpoint. IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` - //UserinfoEndpoint is the URL where an access_token can be used to retrieve the Userinfo. + // UserinfoEndpoint is the URL where an access_token can be used to retrieve the Userinfo. UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` - //RevocationEndpoint is the URL of the OAuth 2.0 Revocation Endpoint. + // RevocationEndpoint is the URL of the OAuth 2.0 Revocation Endpoint. RevocationEndpoint string `json:"revocation_endpoint,omitempty"` - //EndSessionEndpoint is a URL where the RP can perform a redirect to request that the End-User be logged out at the OP. + // EndSessionEndpoint is a URL where the RP can perform a redirect to request that the End-User be logged out at the OP. EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` - //CheckSessionIframe is a URL where the OP provides an iframe that support cross-origin communications for session state information with the RP Client. + // CheckSessionIframe is a URL where the OP provides an iframe that support cross-origin communications for session state information with the RP Client. CheckSessionIframe string `json:"check_session_iframe,omitempty"` - //JwksURI is the URL of the JSON Web Key Set. This site contains the signing keys that RPs can use to validate the signature. - //It may also contain the OP's encryption keys that RPs can use to encrypt request to the OP. + // JwksURI is the URL of the JSON Web Key Set. This site contains the signing keys that RPs can use to validate the signature. + // It may also contain the OP's encryption keys that RPs can use to encrypt request to the OP. JwksURI string `json:"jwks_uri,omitempty"` - //RegistrationEndpoint is the URL for the Dynamic Client Registration. + // RegistrationEndpoint is the URL for the Dynamic Client Registration. RegistrationEndpoint string `json:"registration_endpoint,omitempty"` - //ScopesSupported lists an array of supported scopes. This list must not include every supported scope by the OP. + // ScopesSupported lists an array of supported scopes. This list must not include every supported scope by the OP. ScopesSupported []string `json:"scopes_supported,omitempty"` - //ResponseTypesSupported contains a list of the OAuth 2.0 response_type values that the OP supports (code, id_token, token id_token, ...). + // ResponseTypesSupported contains a list of the OAuth 2.0 response_type values that the OP supports (code, id_token, token id_token, ...). ResponseTypesSupported []string `json:"response_types_supported,omitempty"` - //ResponseModesSupported contains a list of the OAuth 2.0 response_mode values that the OP supports. If omitted, the default value is ["query", "fragment"]. + // ResponseModesSupported contains a list of the OAuth 2.0 response_mode values that the OP supports. If omitted, the default value is ["query", "fragment"]. ResponseModesSupported []string `json:"response_modes_supported,omitempty"` - //GrantTypesSupported contains a list of the OAuth 2.0 grant_type values that the OP supports. If omitted, the default value is ["authorization_code", "implicit"]. + // GrantTypesSupported contains a list of the OAuth 2.0 grant_type values that the OP supports. If omitted, the default value is ["authorization_code", "implicit"]. GrantTypesSupported []GrantType `json:"grant_types_supported,omitempty"` - //ACRValuesSupported contains a list of Authentication Context Class References that the OP supports. + // ACRValuesSupported contains a list of Authentication Context Class References that the OP supports. ACRValuesSupported []string `json:"acr_values_supported,omitempty"` - //SubjectTypesSupported contains a list of Subject Identifier types that the OP supports (pairwise, public). + // SubjectTypesSupported contains a list of Subject Identifier types that the OP supports (pairwise, public). SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` - //IDTokenSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the OP for the ID Token. + // IDTokenSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the OP for the ID Token. IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` - //IDTokenEncryptionAlgValuesSupported contains a list of JWE encryption algorithms (alg values) supported by the OP for the ID Token. + // IDTokenEncryptionAlgValuesSupported contains a list of JWE encryption algorithms (alg values) supported by the OP for the ID Token. IDTokenEncryptionAlgValuesSupported []string `json:"id_token_encryption_alg_values_supported,omitempty"` - //IDTokenEncryptionEncValuesSupported contains a list of JWE encryption algorithms (enc values) supported by the OP for the ID Token. + // IDTokenEncryptionEncValuesSupported contains a list of JWE encryption algorithms (enc values) supported by the OP for the ID Token. IDTokenEncryptionEncValuesSupported []string `json:"id_token_encryption_enc_values_supported,omitempty"` - //UserinfoSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the OP for UserInfo Endpoint. + // UserinfoSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the OP for UserInfo Endpoint. UserinfoSigningAlgValuesSupported []string `json:"userinfo_signing_alg_values_supported,omitempty"` - //UserinfoEncryptionAlgValuesSupported contains a list of JWE encryption algorithms (alg values) supported by the OP for the UserInfo Endpoint. + // UserinfoEncryptionAlgValuesSupported contains a list of JWE encryption algorithms (alg values) supported by the OP for the UserInfo Endpoint. UserinfoEncryptionAlgValuesSupported []string `json:"userinfo_encryption_alg_values_supported,omitempty"` - //UserinfoEncryptionEncValuesSupported contains a list of JWE encryption algorithms (enc values) supported by the OP for the UserInfo Endpoint. + // UserinfoEncryptionEncValuesSupported contains a list of JWE encryption algorithms (enc values) supported by the OP for the UserInfo Endpoint. UserinfoEncryptionEncValuesSupported []string `json:"userinfo_encryption_enc_values_supported,omitempty"` - //RequestObjectSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the OP for Request Objects. - //These algorithms are used both then the Request Object is passed by value (using the request parameter) and when it is passed by reference (using the request_uri parameter). + // RequestObjectSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the OP for Request Objects. + // These algorithms are used both then the Request Object is passed by value (using the request parameter) and when it is passed by reference (using the request_uri parameter). RequestObjectSigningAlgValuesSupported []string `json:"request_object_signing_alg_values_supported,omitempty"` - //RequestObjectEncryptionAlgValuesSupported contains a list of JWE encryption algorithms (alg values) supported by the OP for Request Objects. - //These algorithms are used both when the Request Object is passed by value and by reference. + // RequestObjectEncryptionAlgValuesSupported contains a list of JWE encryption algorithms (alg values) supported by the OP for Request Objects. + // These algorithms are used both when the Request Object is passed by value and by reference. RequestObjectEncryptionAlgValuesSupported []string `json:"request_object_encryption_alg_values_supported,omitempty"` - //RequestObjectEncryptionEncValuesSupported contains a list of JWE encryption algorithms (enc values) supported by the OP for Request Objects. - //These algorithms are used both when the Request Object is passed by value and by reference. + // RequestObjectEncryptionEncValuesSupported contains a list of JWE encryption algorithms (enc values) supported by the OP for Request Objects. + // These algorithms are used both when the Request Object is passed by value and by reference. RequestObjectEncryptionEncValuesSupported []string `json:"request_object_encryption_enc_values_supported,omitempty"` - //TokenEndpointAuthMethodsSupported contains a list of Client Authentication methods supported by the Token Endpoint. If omitted, the default is client_secret_basic. + // TokenEndpointAuthMethodsSupported contains a list of Client Authentication methods supported by the Token Endpoint. If omitted, the default is client_secret_basic. TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"` - //TokenEndpointAuthSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the Token Endpoint - //for the signature of the JWT used to authenticate the Client by private_key_jwt and client_secret_jwt. + // TokenEndpointAuthSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the Token Endpoint + // for the signature of the JWT used to authenticate the Client by private_key_jwt and client_secret_jwt. TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"` - //RevocationEndpointAuthMethodsSupported contains a list of Client Authentication methods supported by the Revocation Endpoint. If omitted, the default is client_secret_basic. + // RevocationEndpointAuthMethodsSupported contains a list of Client Authentication methods supported by the Revocation Endpoint. If omitted, the default is client_secret_basic. RevocationEndpointAuthMethodsSupported []AuthMethod `json:"revocation_endpoint_auth_methods_supported,omitempty"` - //RevocationEndpointAuthSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the Revocation Endpoint - //for the signature of the JWT used to authenticate the Client by private_key_jwt and client_secret_jwt. + // RevocationEndpointAuthSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the Revocation Endpoint + // for the signature of the JWT used to authenticate the Client by private_key_jwt and client_secret_jwt. RevocationEndpointAuthSigningAlgValuesSupported []string `json:"revocation_endpoint_auth_signing_alg_values_supported,omitempty"` - //IntrospectionEndpointAuthMethodsSupported contains a list of Client Authentication methods supported by the Introspection Endpoint. + // IntrospectionEndpointAuthMethodsSupported contains a list of Client Authentication methods supported by the Introspection Endpoint. IntrospectionEndpointAuthMethodsSupported []AuthMethod `json:"introspection_endpoint_auth_methods_supported,omitempty"` - //IntrospectionEndpointAuthSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the Revocation Endpoint - //for the signature of the JWT used to authenticate the Client by private_key_jwt and client_secret_jwt. + // IntrospectionEndpointAuthSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the Revocation Endpoint + // for the signature of the JWT used to authenticate the Client by private_key_jwt and client_secret_jwt. IntrospectionEndpointAuthSigningAlgValuesSupported []string `json:"introspection_endpoint_auth_signing_alg_values_supported,omitempty"` - //DisplayValuesSupported contains a list of display parameter values that the OP supports (page, popup, touch, wap). + // DisplayValuesSupported contains a list of display parameter values that the OP supports (page, popup, touch, wap). DisplayValuesSupported []Display `json:"display_values_supported,omitempty"` - //ClaimTypesSupported contains a list of Claim Types that the OP supports (normal, aggregated, distributed). If omitted, the default is normal Claims. + // ClaimTypesSupported contains a list of Claim Types that the OP supports (normal, aggregated, distributed). If omitted, the default is normal Claims. ClaimTypesSupported []string `json:"claim_types_supported,omitempty"` - //ClaimsSupported contains a list of Claim Names the OP may be able to supply values for. This list might not be exhaustive. + // ClaimsSupported contains a list of Claim Names the OP may be able to supply values for. This list might not be exhaustive. ClaimsSupported []string `json:"claims_supported,omitempty"` - //ClaimsParameterSupported specifies whether the OP supports use of the `claims` parameter. If omitted, the default is false. + // ClaimsParameterSupported specifies whether the OP supports use of the `claims` parameter. If omitted, the default is false. ClaimsParameterSupported bool `json:"claims_parameter_supported,omitempty"` - //CodeChallengeMethodsSupported contains a list of Proof Key for Code Exchange (PKCE) code challenge methods supported by the OP. + // CodeChallengeMethodsSupported contains a list of Proof Key for Code Exchange (PKCE) code challenge methods supported by the OP. CodeChallengeMethodsSupported []CodeChallengeMethod `json:"code_challenge_methods_supported,omitempty"` - //ServiceDocumentation is a URL where developers can get information about the OP and its usage. + // ServiceDocumentation is a URL where developers can get information about the OP and its usage. ServiceDocumentation string `json:"service_documentation,omitempty"` - //ClaimsLocalesSupported contains a list of BCP47 language tag values that the OP supports for values of Claims returned. + // ClaimsLocalesSupported contains a list of BCP47 language tag values that the OP supports for values of Claims returned. ClaimsLocalesSupported []language.Tag `json:"claims_locales_supported,omitempty"` - //UILocalesSupported contains a list of BCP47 language tag values that the OP supports for the user interface. + // UILocalesSupported contains a list of BCP47 language tag values that the OP supports for the user interface. UILocalesSupported []language.Tag `json:"ui_locales_supported,omitempty"` - //RequestParameterSupported specifies whether the OP supports use of the `request` parameter. If omitted, the default value is false. + // RequestParameterSupported specifies whether the OP supports use of the `request` parameter. If omitted, the default value is false. RequestParameterSupported bool `json:"request_parameter_supported,omitempty"` - //RequestURIParameterSupported specifies whether the OP supports use of the `request_uri` parameter. If omitted, the default value is true. (therefore no omitempty) + // RequestURIParameterSupported specifies whether the OP supports use of the `request_uri` parameter. If omitted, the default value is true. (therefore no omitempty) RequestURIParameterSupported bool `json:"request_uri_parameter_supported"` - //RequireRequestURIRegistration specifies whether the OP requires any `request_uri` to be pre-registered using the request_uris registration parameter. If omitted, the default value is false. + // RequireRequestURIRegistration specifies whether the OP requires any `request_uri` to be pre-registered using the request_uris registration parameter. If omitted, the default value is false. RequireRequestURIRegistration bool `json:"require_request_uri_registration,omitempty"` - //OPPolicyURI is a URL the OP provides to the person registering the Client to read about the OP's requirements on how the RP can use the data provided by the OP. + // OPPolicyURI is a URL the OP provides to the person registering the Client to read about the OP's requirements on how the RP can use the data provided by the OP. OPPolicyURI string `json:"op_policy_uri,omitempty"` - //OPTermsOfServiceURI is a URL the OpenID Provider provides to the person registering the Client to read about OpenID Provider's terms of service. + // OPTermsOfServiceURI is a URL the OpenID Provider provides to the person registering the Client to read about OpenID Provider's terms of service. OPTermsOfServiceURI string `json:"op_tos_uri,omitempty"` } diff --git a/pkg/oidc/grants/client_credentials.go b/pkg/oidc/grants/client_credentials.go index e9f467e..8cb8425 100644 --- a/pkg/oidc/grants/client_credentials.go +++ b/pkg/oidc/grants/client_credentials.go @@ -13,8 +13,8 @@ type clientCredentialsGrant struct { clientSecret string `schema:"client_secret"` } -//ClientCredentialsGrantBasic creates an oauth2 `Client Credentials` Grant -//sending client_id and client_secret as basic auth header +// ClientCredentialsGrantBasic creates an oauth2 `Client Credentials` Grant +// sending client_id and client_secret as basic auth header func ClientCredentialsGrantBasic(scopes ...string) *clientCredentialsGrantBasic { return &clientCredentialsGrantBasic{ grantType: "client_credentials", @@ -22,8 +22,8 @@ func ClientCredentialsGrantBasic(scopes ...string) *clientCredentialsGrantBasic } } -//ClientCredentialsGrantValues creates an oauth2 `Client Credentials` Grant -//sending client_id and client_secret as form values +// ClientCredentialsGrantValues creates an oauth2 `Client Credentials` Grant +// sending client_id and client_secret as form values func ClientCredentialsGrantValues(clientID, clientSecret string, scopes ...string) *clientCredentialsGrant { return &clientCredentialsGrant{ clientCredentialsGrantBasic: ClientCredentialsGrantBasic(scopes...), diff --git a/pkg/oidc/jwt_profile.go b/pkg/oidc/jwt_profile.go index 25b7caa..66fa3aa 100644 --- a/pkg/oidc/jwt_profile.go +++ b/pkg/oidc/jwt_profile.go @@ -6,9 +6,9 @@ type JWTProfileGrantRequest struct { GrantType GrantType `schema:"grant_type"` } -//NewJWTProfileGrantRequest creates an oauth2 `JSON Web Token (JWT) Profile` Grant +// NewJWTProfileGrantRequest creates an oauth2 `JSON Web Token (JWT) Profile` Grant //`urn:ietf:params:oauth:grant-type:jwt-bearer` -//sending a self-signed jwt as assertion +// sending a self-signed jwt as assertion func NewJWTProfileGrantRequest(assertion string, scopes ...string) *JWTProfileGrantRequest { return &JWTProfileGrantRequest{ GrantType: GrantTypeBearer, diff --git a/pkg/oidc/keyset.go b/pkg/oidc/keyset.go index 57fe526..c6e865b 100644 --- a/pkg/oidc/keyset.go +++ b/pkg/oidc/keyset.go @@ -19,16 +19,16 @@ var ( ErrKeyNone = errors.New("no possible keys matches") ) -//KeySet represents a set of JSON Web Keys +// KeySet represents a set of JSON Web Keys // - remotely fetch via discovery and jwks_uri -> `remoteKeySet` // - held by the OP itself in storage -> `openIDKeySet` // - dynamically aggregated by request for OAuth JWT Profile Assertion -> `jwtProfileKeySet` type KeySet interface { - //VerifySignature verifies the signature with the given keyset and returns the raw payload + // VerifySignature verifies the signature with the given keyset and returns the raw payload VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) } -//GetKeyIDAndAlg returns the `kid` and `alg` claim from the JWS header +// GetKeyIDAndAlg returns the `kid` and `alg` claim from the JWS header func GetKeyIDAndAlg(jws *jose.JSONWebSignature) (string, string) { keyID := "" alg := "" @@ -40,11 +40,11 @@ func GetKeyIDAndAlg(jws *jose.JSONWebSignature) (string, string) { return keyID, alg } -//FindKey searches the given JSON Web Keys for the requested key ID, usage and key type +// FindKey searches the given JSON Web Keys for the requested key ID, usage and key type // -//will return the key immediately if matches exact (id, usage, type) +// will return the key immediately if matches exact (id, usage, type) // -//will return false none or multiple match +// will return false none or multiple match // //deprecated: use FindMatchingKey which will return an error (more specific) instead of just a bool //moved implementation already to FindMatchingKey @@ -53,35 +53,35 @@ func FindKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (jose.JSON return key, err == nil } -//FindMatchingKey searches the given JSON Web Keys for the requested key ID, usage and alg type +// FindMatchingKey searches the given JSON Web Keys for the requested key ID, usage and alg type // -//will return the key immediately if matches exact (id, usage, type) +// will return the key immediately if matches exact (id, usage, type) // -//will return a specific error if none (ErrKeyNone) or multiple (ErrKeyMultiple) match +// will return a specific error if none (ErrKeyNone) or multiple (ErrKeyMultiple) match func FindMatchingKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (key jose.JSONWebKey, err error) { var validKeys []jose.JSONWebKey for _, k := range keys { - //ignore all keys with wrong use (let empty use of published key pass) + // ignore all keys with wrong use (let empty use of published key pass) if k.Use != use && k.Use != "" { continue } - //ignore all keys with wrong algorithm type + // ignore all keys with wrong algorithm type if !algToKeyType(k.Key, expectedAlg) { continue } - //if we get here, use and alg match, so an equal (not empty) keyID is an exact match + // if we get here, use and alg match, so an equal (not empty) keyID is an exact match if k.KeyID == keyID && keyID != "" { return k, nil } - //keyIDs did not match or at least one was empty (if later, then it could be a match) + // keyIDs did not match or at least one was empty (if later, then it could be a match) if k.KeyID == "" || keyID == "" { validKeys = append(validKeys, k) } } - //if we get here, no match was possible at all (use / alg) or no exact match due to - //the signed JWT and / or the published keys didn't have a kid - //if later applies and only one key could be found, we'll return it - //otherwise a corresponding error will be thrown + // if we get here, no match was possible at all (use / alg) or no exact match due to + // the signed JWT and / or the published keys didn't have a kid + // if later applies and only one key could be found, we'll return it + // otherwise a corresponding error will be thrown if len(validKeys) == 1 { return validKeys[0], nil } diff --git a/pkg/oidc/session.go b/pkg/oidc/session.go index 9aa70c1..b470d1e 100644 --- a/pkg/oidc/session.go +++ b/pkg/oidc/session.go @@ -1,6 +1,6 @@ package oidc -//EndSessionRequest for the RP-Initiated Logout according to: +// EndSessionRequest for the RP-Initiated Logout according to: //https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout type EndSessionRequest struct { IdTokenHint string `schema:"id_token_hint"` diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index 9c8c48b..784684d 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -14,7 +14,7 @@ import ( ) const ( - //BearerToken defines the token_type `Bearer`, which is returned in a successful token response + // BearerToken defines the token_type `Bearer`, which is returned in a successful token response BearerToken = "Bearer" PrefixBearer = BearerToken + " " @@ -91,62 +91,62 @@ type accessTokenClaims struct { signatureAlg jose.SignatureAlgorithm `json:"-"` } -//GetIssuer implements the Claims interface +// GetIssuer implements the Claims interface func (a *accessTokenClaims) GetIssuer() string { return a.Issuer } -//GetAudience implements the Claims interface +// GetAudience implements the Claims interface func (a *accessTokenClaims) GetAudience() []string { return a.Audience } -//GetExpiration implements the Claims interface +// GetExpiration implements the Claims interface func (a *accessTokenClaims) GetExpiration() time.Time { return time.Time(a.Expiration) } -//GetIssuedAt implements the Claims interface +// GetIssuedAt implements the Claims interface func (a *accessTokenClaims) GetIssuedAt() time.Time { return time.Time(a.IssuedAt) } -//GetNonce implements the Claims interface +// GetNonce implements the Claims interface func (a *accessTokenClaims) GetNonce() string { return a.Nonce } -//GetAuthenticationContextClassReference implements the Claims interface +// GetAuthenticationContextClassReference implements the Claims interface func (a *accessTokenClaims) GetAuthenticationContextClassReference() string { return a.AuthenticationContextClassReference } -//GetAuthTime implements the Claims interface +// GetAuthTime implements the Claims interface func (a *accessTokenClaims) GetAuthTime() time.Time { return time.Time(a.AuthTime) } -//GetAuthorizedParty implements the Claims interface +// GetAuthorizedParty implements the Claims interface func (a *accessTokenClaims) GetAuthorizedParty() string { return a.AuthorizedParty } -//SetSignatureAlgorithm implements the Claims interface +// SetSignatureAlgorithm implements the Claims interface func (a *accessTokenClaims) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) { a.signatureAlg = algorithm } -//GetSubject implements the AccessTokenClaims interface +// GetSubject implements the AccessTokenClaims interface func (a *accessTokenClaims) GetSubject() string { return a.Subject } -//GetTokenID implements the AccessTokenClaims interface +// GetTokenID implements the AccessTokenClaims interface func (a *accessTokenClaims) GetTokenID() string { return a.JWTID } -//SetPrivateClaims implements the AccessTokenClaims interface +// SetPrivateClaims implements the AccessTokenClaims interface func (a *accessTokenClaims) SetPrivateClaims(claims map[string]interface{}) { a.claims = claims } @@ -243,97 +243,97 @@ type idTokenClaims struct { signatureAlg jose.SignatureAlgorithm } -//GetIssuer implements the Claims interface +// GetIssuer implements the Claims interface func (t *idTokenClaims) GetIssuer() string { return t.Issuer } -//GetAudience implements the Claims interface +// GetAudience implements the Claims interface func (t *idTokenClaims) GetAudience() []string { return t.Audience } -//GetExpiration implements the Claims interface +// GetExpiration implements the Claims interface func (t *idTokenClaims) GetExpiration() time.Time { return time.Time(t.Expiration) } -//GetIssuedAt implements the Claims interface +// GetIssuedAt implements the Claims interface func (t *idTokenClaims) GetIssuedAt() time.Time { return time.Time(t.IssuedAt) } -//GetNonce implements the Claims interface +// GetNonce implements the Claims interface func (t *idTokenClaims) GetNonce() string { return t.Nonce } -//GetAuthenticationContextClassReference implements the Claims interface +// GetAuthenticationContextClassReference implements the Claims interface func (t *idTokenClaims) GetAuthenticationContextClassReference() string { return t.AuthenticationContextClassReference } -//GetAuthTime implements the Claims interface +// GetAuthTime implements the Claims interface func (t *idTokenClaims) GetAuthTime() time.Time { return time.Time(t.AuthTime) } -//GetAuthorizedParty implements the Claims interface +// GetAuthorizedParty implements the Claims interface func (t *idTokenClaims) GetAuthorizedParty() string { return t.AuthorizedParty } -//SetSignatureAlgorithm implements the Claims interface +// SetSignatureAlgorithm implements the Claims interface func (t *idTokenClaims) SetSignatureAlgorithm(alg jose.SignatureAlgorithm) { t.signatureAlg = alg } -//GetNotBefore implements the IDTokenClaims interface +// GetNotBefore implements the IDTokenClaims interface func (t *idTokenClaims) GetNotBefore() time.Time { return time.Time(t.NotBefore) } -//GetJWTID implements the IDTokenClaims interface +// GetJWTID implements the IDTokenClaims interface func (t *idTokenClaims) GetJWTID() string { return t.JWTID } -//GetAccessTokenHash implements the IDTokenClaims interface +// GetAccessTokenHash implements the IDTokenClaims interface func (t *idTokenClaims) GetAccessTokenHash() string { return t.AccessTokenHash } -//GetCodeHash implements the IDTokenClaims interface +// GetCodeHash implements the IDTokenClaims interface func (t *idTokenClaims) GetCodeHash() string { return t.CodeHash } -//GetAuthenticationMethodsReferences implements the IDTokenClaims interface +// GetAuthenticationMethodsReferences implements the IDTokenClaims interface func (t *idTokenClaims) GetAuthenticationMethodsReferences() []string { return t.AuthenticationMethodsReferences } -//GetClientID implements the IDTokenClaims interface +// GetClientID implements the IDTokenClaims interface func (t *idTokenClaims) GetClientID() string { return t.ClientID } -//GetSignatureAlgorithm implements the IDTokenClaims interface +// GetSignatureAlgorithm implements the IDTokenClaims interface func (t *idTokenClaims) GetSignatureAlgorithm() jose.SignatureAlgorithm { return t.signatureAlg } -//SetAccessTokenHash implements the IDTokenClaims interface +// SetAccessTokenHash implements the IDTokenClaims interface func (t *idTokenClaims) SetAccessTokenHash(hash string) { t.AccessTokenHash = hash } -//SetUserinfo implements the IDTokenClaims interface +// SetUserinfo implements the IDTokenClaims interface func (t *idTokenClaims) SetUserinfo(info UserInfo) { t.UserInfo = info } -//SetCodeHash implements the IDTokenClaims interface +// SetCodeHash implements the IDTokenClaims interface func (t *idTokenClaims) SetCodeHash(hash string) { t.CodeHash = hash } diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index a25da9c..ec11057 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -9,33 +9,34 @@ import ( ) const ( - //GrantTypeCode defines the grant_type `authorization_code` used for the Token Request in the Authorization Code Flow + // GrantTypeCode defines the grant_type `authorization_code` used for the Token Request in the Authorization Code Flow GrantTypeCode GrantType = "authorization_code" - //GrantTypeRefreshToken defines the grant_type `refresh_token` used for the Token Request in the Refresh Token Flow + // GrantTypeRefreshToken defines the grant_type `refresh_token` used for the Token Request in the Refresh Token Flow GrantTypeRefreshToken GrantType = "refresh_token" - //GrantTypeClientCredentials defines the grant_type `client_credentials` used for the Token Request in the Client Credentials Token Flow + // GrantTypeClientCredentials defines the grant_type `client_credentials` used for the Token Request in the Client Credentials Token Flow GrantTypeClientCredentials GrantType = "client_credentials" - //GrantTypeBearer defines the grant_type `urn:ietf:params:oauth:grant-type:jwt-bearer` used for the JWT Authorization Grant + // GrantTypeBearer defines the grant_type `urn:ietf:params:oauth:grant-type:jwt-bearer` used for the JWT Authorization Grant GrantTypeBearer GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" - //GrantTypeTokenExchange defines the grant_type `urn:ietf:params:oauth:grant-type:token-exchange` used for the OAuth Token Exchange Grant + // GrantTypeTokenExchange defines the grant_type `urn:ietf:params:oauth:grant-type:token-exchange` used for the OAuth Token Exchange Grant GrantTypeTokenExchange GrantType = "urn:ietf:params:oauth:grant-type:token-exchange" - //GrantTypeImplicit defines the grant type `implicit` used for implicit flows that skip the generation and exchange of an Authorization Code + // GrantTypeImplicit defines the grant type `implicit` used for implicit flows that skip the generation and exchange of an Authorization Code GrantTypeImplicit GrantType = "implicit" - //ClientAssertionTypeJWTAssertion defines the client_assertion_type `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` - //used for the OAuth JWT Profile Client Authentication + // ClientAssertionTypeJWTAssertion defines the client_assertion_type `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` + // used for the OAuth JWT Profile Client Authentication ClientAssertionTypeJWTAssertion = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" ) var AllGrantTypes = []GrantType{ GrantTypeCode, GrantTypeRefreshToken, GrantTypeClientCredentials, GrantTypeBearer, GrantTypeTokenExchange, GrantTypeImplicit, - ClientAssertionTypeJWTAssertion} + ClientAssertionTypeJWTAssertion, +} type GrantType string @@ -60,12 +61,12 @@ func (a *AccessTokenRequest) GrantType() GrantType { return GrantTypeCode } -//SetClientID implements op.AuthenticatedTokenRequest +// SetClientID implements op.AuthenticatedTokenRequest func (a *AccessTokenRequest) SetClientID(clientID string) { a.ClientID = clientID } -//SetClientSecret implements op.AuthenticatedTokenRequest +// SetClientSecret implements op.AuthenticatedTokenRequest func (a *AccessTokenRequest) SetClientSecret(clientSecret string) { a.ClientSecret = clientSecret } @@ -85,12 +86,12 @@ func (a *RefreshTokenRequest) GrantType() GrantType { return GrantTypeRefreshToken } -//SetClientID implements op.AuthenticatedTokenRequest +// SetClientID implements op.AuthenticatedTokenRequest func (a *RefreshTokenRequest) SetClientID(clientID string) { a.ClientID = clientID } -//SetClientSecret implements op.AuthenticatedTokenRequest +// SetClientSecret implements op.AuthenticatedTokenRequest func (a *RefreshTokenRequest) SetClientSecret(clientSecret string) { a.ClientSecret = clientSecret } @@ -148,55 +149,55 @@ func (j *JWTTokenRequest) GetCustomClaim(key string) interface{} { return j.private[key] } -//GetIssuer implements the Claims interface +// GetIssuer implements the Claims interface func (j *JWTTokenRequest) GetIssuer() string { return j.Issuer } -//GetAudience implements the Claims and TokenRequest interfaces +// GetAudience implements the Claims and TokenRequest interfaces func (j *JWTTokenRequest) GetAudience() []string { return j.Audience } -//GetExpiration implements the Claims interface +// GetExpiration implements the Claims interface func (j *JWTTokenRequest) GetExpiration() time.Time { return time.Time(j.ExpiresAt) } -//GetIssuedAt implements the Claims interface +// GetIssuedAt implements the Claims interface func (j *JWTTokenRequest) GetIssuedAt() time.Time { return time.Time(j.IssuedAt) } -//GetNonce implements the Claims interface +// GetNonce implements the Claims interface func (j *JWTTokenRequest) GetNonce() string { return "" } -//GetAuthenticationContextClassReference implements the Claims interface +// GetAuthenticationContextClassReference implements the Claims interface func (j *JWTTokenRequest) GetAuthenticationContextClassReference() string { return "" } -//GetAuthTime implements the Claims interface +// GetAuthTime implements the Claims interface func (j *JWTTokenRequest) GetAuthTime() time.Time { return time.Time{} } -//GetAuthorizedParty implements the Claims interface +// GetAuthorizedParty implements the Claims interface func (j *JWTTokenRequest) GetAuthorizedParty() string { return "" } -//SetSignatureAlgorithm implements the Claims interface +// SetSignatureAlgorithm implements the Claims interface func (j *JWTTokenRequest) SetSignatureAlgorithm(_ jose.SignatureAlgorithm) {} -//GetSubject implements the TokenRequest interface +// GetSubject implements the TokenRequest interface func (j *JWTTokenRequest) GetSubject() string { return j.Subject } -//GetScopes implements the TokenRequest interface +// GetScopes implements the TokenRequest interface func (j *JWTTokenRequest) GetScopes() []string { return j.Scopes } diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go index 474e52e..cc18c80 100644 --- a/pkg/oidc/verifier.go +++ b/pkg/oidc/verifier.go @@ -61,11 +61,11 @@ type Verifier interface { Offset() time.Duration } -//ACRVerifier specifies the function to be used by the `DefaultVerifier` for validating the acr claim +// ACRVerifier specifies the function to be used by the `DefaultVerifier` for validating the acr claim type ACRVerifier func(string) error -//DefaultACRVerifier implements `ACRVerifier` returning an error -//if none of the provided values matches the acr claim +// DefaultACRVerifier implements `ACRVerifier` returning an error +// if none of the provided values matches the acr claim func DefaultACRVerifier(possibleValues []string) ACRVerifier { return func(acr string) error { if !str.Contains(possibleValues, acr) { @@ -76,7 +76,7 @@ func DefaultACRVerifier(possibleValues []string) ACRVerifier { } func DecryptToken(tokenString string) (string, error) { - return tokenString, nil //TODO: impl + return tokenString, nil // TODO: impl } func ParseToken(tokenString string, claims interface{}) ([]byte, error) { @@ -111,7 +111,7 @@ func CheckAudience(claims Claims, clientID string) error { return fmt.Errorf("%w: Audience must contain client_id %q", ErrAudience, clientID) } - //TODO: check aud trusted + // TODO: check aud trusted return nil } @@ -202,6 +202,7 @@ func CheckAuthorizationContextClassReference(claims Claims, acr ACRVerifier) err } return nil } + func CheckAuthTime(claims Claims, maxAge time.Duration) error { if maxAge == 0 { return nil diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 979fe62..9dc07d2 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -45,8 +45,8 @@ type Authorizer interface { RequestObjectSupported() bool } -//AuthorizeValidator is an extension of Authorizer interface -//implementing its own validation mechanism for the auth request +// AuthorizeValidator is an extension of Authorizer interface +// implementing its own validation mechanism for the auth request type AuthorizeValidator interface { Authorizer ValidateAuthRequest(context.Context, *oidc.AuthRequest, Storage, IDTokenHintVerifier) (string, error) @@ -64,8 +64,8 @@ func authorizeCallbackHandler(authorizer Authorizer) func(http.ResponseWriter, * } } -//Authorize handles the authorization request, including -//parsing, validating, storing and finally redirecting to the login handler +// Authorize handles the authorization request, including +// parsing, validating, storing and finally redirecting to the login handler func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { authReq, err := ParseAuthorizeRequest(r, authorizer.Decoder()) if err != nil { @@ -113,7 +113,7 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { RedirectToLogin(req.GetID(), client, w, r) } -//ParseAuthorizeRequest parsed the http request into an oidc.AuthRequest +// ParseAuthorizeRequest parsed the http request into an oidc.AuthRequest func ParseAuthorizeRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.AuthRequest, error) { err := r.ParseForm() if err != nil { @@ -127,8 +127,8 @@ func ParseAuthorizeRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.A return authReq, nil } -//ParseRequestObject parse the `request` parameter, validates the token including the signature -//and copies the token claims into the auth request +// ParseRequestObject parse the `request` parameter, validates the token including the signature +// and copies the token claims into the auth request func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, issuer string) (*oidc.AuthRequest, error) { requestObject := new(oidc.RequestObject) payload, err := oidc.ParseToken(authReq.RequestParam, requestObject) @@ -156,8 +156,8 @@ func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage return authReq, nil } -//CopyRequestObjectToAuthRequest overwrites present values from the Request Object into the auth request -//and clears the `RequestParam` of the auth request +// CopyRequestObjectToAuthRequest overwrites present values from the Request Object into the auth request +// and clears the `RequestParam` of the auth request func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oidc.RequestObject) { if str.Contains(authReq.Scopes, oidc.ScopeOpenID) && len(requestObject.Scopes) > 0 { authReq.Scopes = requestObject.Scopes @@ -204,7 +204,7 @@ func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oi authReq.RequestParam = "" } -//ValidateAuthRequest validates the authorize parameters and returns the userID of the id_token_hint if passed +// ValidateAuthRequest validates the authorize parameters and returns the userID of the id_token_hint if passed func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier IDTokenHintVerifier) (sub string, err error) { authReq.MaxAge, err = ValidateAuthReqPrompt(authReq.Prompt, authReq.MaxAge) if err != nil { @@ -227,7 +227,7 @@ func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage return ValidateAuthReqIDTokenHint(ctx, authReq.IDTokenHint, verifier) } -//ValidateAuthReqPrompt validates the passed prompt values and sets max_age to 0 if prompt login is present +// ValidateAuthReqPrompt validates the passed prompt values and sets max_age to 0 if prompt login is present func ValidateAuthReqPrompt(prompts []string, maxAge *uint) (_ *uint, err error) { for _, prompt := range prompts { if prompt == oidc.PromptNone && len(prompts) > 1 { @@ -240,7 +240,7 @@ func ValidateAuthReqPrompt(prompts []string, maxAge *uint) (_ *uint, err error) return maxAge, nil } -//ValidateAuthReqScopes validates the passed scopes +// ValidateAuthReqScopes validates the passed scopes func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) { if len(scopes) == 0 { return nil, oidc.ErrInvalidRequest(). @@ -274,7 +274,7 @@ func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) { return scopes, nil } -//ValidateAuthReqRedirectURI validates the passed redirect_uri and response_type to the registered uris and client type +// ValidateAuthReqRedirectURI validates the passed redirect_uri and response_type to the registered uris and client type func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.ResponseType) error { if uri == "" { return oidc.ErrInvalidRequestRedirectURI().WithDescription("The redirect_uri is missing in the request. " + @@ -309,7 +309,7 @@ func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.Res "If you have any questions, you may contact the administrator of the application.") } -//ValidateAuthReqRedirectURINative validates the passed redirect_uri and response_type to the registered uris and client type +// ValidateAuthReqRedirectURINative validates the passed redirect_uri and response_type to the registered uris and client type func validateAuthReqRedirectURINative(client Client, uri string, responseType oidc.ResponseType) error { parsedURL, isLoopback := HTTPLoopbackOrLocalhost(uri) isCustomSchema := !strings.HasPrefix(uri, "http://") @@ -350,7 +350,7 @@ func HTTPLoopbackOrLocalhost(rawurl string) (*url.URL, bool) { return parsedURL, hostName == "localhost" || net.ParseIP(hostName).IsLoopback() } -//ValidateAuthReqResponseType validates the passed response_type to the registered response types +// ValidateAuthReqResponseType validates the passed response_type to the registered response types func ValidateAuthReqResponseType(client Client, responseType oidc.ResponseType) error { if responseType == "" { return oidc.ErrInvalidRequest().WithDescription("The response type is missing in your request. " + @@ -363,8 +363,8 @@ func ValidateAuthReqResponseType(client Client, responseType oidc.ResponseType) return nil } -//ValidateAuthReqIDTokenHint validates the id_token_hint (if passed as parameter in the request) -//and returns the `sub` claim +// ValidateAuthReqIDTokenHint validates the id_token_hint (if passed as parameter in the request) +// and returns the `sub` claim func ValidateAuthReqIDTokenHint(ctx context.Context, idTokenHint string, verifier IDTokenHintVerifier) (string, error) { if idTokenHint == "" { return "", nil @@ -377,13 +377,13 @@ func ValidateAuthReqIDTokenHint(ctx context.Context, idTokenHint string, verifie return claims.GetSubject(), nil } -//RedirectToLogin redirects the end user to the Login UI for authentication +// RedirectToLogin redirects the end user to the Login UI for authentication func RedirectToLogin(authReqID string, client Client, w http.ResponseWriter, r *http.Request) { login := client.LoginURL(authReqID) http.Redirect(w, r, login, http.StatusFound) } -//AuthorizeCallback handles the callback after authentication in the Login UI +// AuthorizeCallback handles the callback after authentication in the Login UI func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { params := mux.Vars(r) id := params["id"] @@ -406,7 +406,7 @@ func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Author AuthResponse(authReq, authorizer, w, r) } -//AuthResponse creates the successful authentication response (either code or tokens) +// AuthResponse creates the successful authentication response (either code or tokens) func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWriter, r *http.Request) { client, err := authorizer.Storage().GetClientByClientID(r.Context(), authReq.GetClientID()) if err != nil { @@ -420,7 +420,7 @@ func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWri AuthResponseToken(w, r, authReq, authorizer, client) } -//AuthResponseCode creates the successful code authentication response +// AuthResponseCode creates the successful code authentication response func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer) { code, err := CreateAuthRequestCode(r.Context(), authReq, authorizer.Storage(), authorizer.Crypto()) if err != nil { @@ -442,7 +442,7 @@ func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthReques http.Redirect(w, r, callback, http.StatusFound) } -//AuthResponseToken creates the successful token(s) authentication response +// AuthResponseToken creates the successful token(s) authentication response func AuthResponseToken(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer, client Client) { createAccessToken := authReq.GetResponseType() != oidc.ResponseTypeIDTokenOnly resp, err := CreateTokenResponse(r.Context(), authReq, client, authorizer, createAccessToken, "", "") @@ -458,7 +458,7 @@ func AuthResponseToken(w http.ResponseWriter, r *http.Request, authReq AuthReque http.Redirect(w, r, callback, http.StatusFound) } -//CreateAuthRequestCode creates and stores a code for the auth code response +// CreateAuthRequestCode creates and stores a code for the auth code response func CreateAuthRequestCode(ctx context.Context, authReq AuthRequest, storage Storage, crypto Crypto) (string, error) { code, err := BuildAuthRequestCode(authReq, crypto) if err != nil { @@ -470,13 +470,13 @@ func CreateAuthRequestCode(ctx context.Context, authReq AuthRequest, storage Sto return code, nil } -//BuildAuthRequestCode builds the string representation of the auth code +// BuildAuthRequestCode builds the string representation of the auth code func BuildAuthRequestCode(authReq AuthRequest, crypto Crypto) (string, error) { return crypto.Encrypt(authReq.GetID()) } -//AuthResponseURL encodes the authorization response (successful and error) and sets it as query or fragment values -//depending on the response_mode and response_type +// AuthResponseURL encodes the authorization response (successful and error) and sets it as query or fragment values +// depending on the response_mode and response_type func AuthResponseURL(redirectURI string, responseType oidc.ResponseType, responseMode oidc.ResponseMode, response interface{}, encoder httphelper.Encoder) (string, error) { uri, err := url.Parse(redirectURI) if err != nil { @@ -486,18 +486,18 @@ func AuthResponseURL(redirectURI string, responseType oidc.ResponseType, respons if err != nil { return "", oidc.ErrServerError().WithParent(err) } - //return explicitly requested mode + // return explicitly requested mode if responseMode == oidc.ResponseModeQuery { return mergeQueryParams(uri, params), nil } if responseMode == oidc.ResponseModeFragment { return setFragment(uri, params), nil } - //implicit must use fragment mode is not specified by client + // implicit must use fragment mode is not specified by client if responseType == oidc.ResponseTypeIDToken || responseType == oidc.ResponseTypeIDTokenOnly { return setFragment(uri, params), nil } - //if we get here it's code flow: defaults to query + // if we get here it's code flow: defaults to query return mergeQueryParams(uri, params), nil } diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index 9023011..dc6f655 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -20,8 +20,8 @@ import ( ) // -//TOOD: tests will be implemented in branch for service accounts -//func TestAuthorize(t *testing.T) { +// TOOD: tests will be implemented in branch for service accounts +// func TestAuthorize(t *testing.T) { // // testCallback := func(t *testing.T, clienID string) callbackHandler { // // return func(authReq *oidc.AuthRequest, client oidc.Client, w http.ResponseWriter, r *http.Request) { // // // require.Equal(t, clientID, client.) @@ -364,191 +364,245 @@ func TestValidateAuthReqRedirectURI(t *testing.T) { }{ { "empty fails", - args{"", + args{ + "", mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeWeb, nil, false), - oidc.ResponseTypeCode}, + oidc.ResponseTypeCode, + }, true, }, { "unregistered https fails", - args{"https://unregistered.com/callback", + args{ + "https://unregistered.com/callback", mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeWeb, nil, false), - oidc.ResponseTypeCode}, + oidc.ResponseTypeCode, + }, true, }, { "unregistered http fails", - args{"http://unregistered.com/callback", + args{ + "http://unregistered.com/callback", mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeWeb, nil, false), - oidc.ResponseTypeCode}, + oidc.ResponseTypeCode, + }, true, }, { "code flow registered https web ok", - args{"https://registered.com/callback", + args{ + "https://registered.com/callback", mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeWeb, nil, false), - oidc.ResponseTypeCode}, + oidc.ResponseTypeCode, + }, false, }, { "code flow registered https native ok", - args{"https://registered.com/callback", + args{ + "https://registered.com/callback", mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeNative, nil, false), - oidc.ResponseTypeCode}, + oidc.ResponseTypeCode, + }, false, }, { "code flow registered https user agent ok", - args{"https://registered.com/callback", + args{ + "https://registered.com/callback", mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, false), - oidc.ResponseTypeCode}, + oidc.ResponseTypeCode, + }, false, }, { "code flow registered http confidential (web) ok", - args{"http://registered.com/callback", + args{ + "http://registered.com/callback", mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeWeb, nil, false), - oidc.ResponseTypeCode}, + oidc.ResponseTypeCode, + }, false, }, { "code flow registered http not confidential (native) fails", - args{"http://registered.com/callback", + args{ + "http://registered.com/callback", mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeNative, nil, false), - oidc.ResponseTypeCode}, + oidc.ResponseTypeCode, + }, true, }, { "code flow registered http not confidential (user agent) fails", - args{"http://registered.com/callback", + args{ + "http://registered.com/callback", mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, false), - oidc.ResponseTypeCode}, + oidc.ResponseTypeCode, + }, true, }, { "code flow registered http localhost native ok", - args{"http://localhost:4200/callback", + args{ + "http://localhost:4200/callback", mock.NewClientWithConfig(t, []string{"http://localhost/callback"}, op.ApplicationTypeNative, nil, false), - oidc.ResponseTypeCode}, + oidc.ResponseTypeCode, + }, false, }, { "code flow registered http loopback v4 native ok", - args{"http://127.0.0.1:4200/callback", + args{ + "http://127.0.0.1:4200/callback", mock.NewClientWithConfig(t, []string{"http://127.0.0.1/callback"}, op.ApplicationTypeNative, nil, false), - oidc.ResponseTypeCode}, + oidc.ResponseTypeCode, + }, false, }, { "code flow registered http loopback v6 native ok", - args{"http://[::1]:4200/callback", + args{ + "http://[::1]:4200/callback", mock.NewClientWithConfig(t, []string{"http://[::1]/callback"}, op.ApplicationTypeNative, nil, false), - oidc.ResponseTypeCode}, + oidc.ResponseTypeCode, + }, false, }, { "code flow unregistered http native fails", - args{"http://unregistered.com/callback", + args{ + "http://unregistered.com/callback", mock.NewClientWithConfig(t, []string{"http://locahost/callback"}, op.ApplicationTypeNative, nil, false), - oidc.ResponseTypeCode}, + oidc.ResponseTypeCode, + }, true, }, { "code flow unregistered custom native fails", - args{"unregistered://callback", + args{ + "unregistered://callback", mock.NewClientWithConfig(t, []string{"registered://callback"}, op.ApplicationTypeNative, nil, false), - oidc.ResponseTypeCode}, + oidc.ResponseTypeCode, + }, true, }, { "code flow unregistered loopback native fails", - args{"http://[::1]:4200/unregistered", + args{ + "http://[::1]:4200/unregistered", mock.NewClientWithConfig(t, []string{"http://[::1]:4200/callback"}, op.ApplicationTypeNative, nil, false), - oidc.ResponseTypeCode}, + oidc.ResponseTypeCode, + }, true, }, { "code flow registered custom not native (web) fails", - args{"custom://callback", + args{ + "custom://callback", mock.NewClientWithConfig(t, []string{"custom://callback"}, op.ApplicationTypeWeb, nil, false), - oidc.ResponseTypeCode}, + oidc.ResponseTypeCode, + }, true, }, { "code flow registered custom not native (user agent) fails", - args{"custom://callback", + args{ + "custom://callback", mock.NewClientWithConfig(t, []string{"custom://callback"}, op.ApplicationTypeUserAgent, nil, false), - oidc.ResponseTypeCode}, + oidc.ResponseTypeCode, + }, true, }, { "code flow registered custom native ok", - args{"custom://callback", + args{ + "custom://callback", mock.NewClientWithConfig(t, []string{"custom://callback"}, op.ApplicationTypeNative, nil, false), - oidc.ResponseTypeCode}, + oidc.ResponseTypeCode, + }, false, }, { "code flow dev mode http ok", - args{"http://registered.com/callback", + args{ + "http://registered.com/callback", mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, true), - oidc.ResponseTypeCode}, + oidc.ResponseTypeCode, + }, false, }, { "implicit flow registered ok", - args{"https://registered.com/callback", + args{ + "https://registered.com/callback", mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, false), - oidc.ResponseTypeIDToken}, + oidc.ResponseTypeIDToken, + }, false, }, { "implicit flow unregistered fails", - args{"https://unregistered.com/callback", + args{ + "https://unregistered.com/callback", mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, false), - oidc.ResponseTypeIDToken}, + oidc.ResponseTypeIDToken, + }, true, }, { "implicit flow registered http localhost native ok", - args{"http://localhost:9999/callback", + args{ + "http://localhost:9999/callback", mock.NewClientWithConfig(t, []string{"http://localhost:9999/callback"}, op.ApplicationTypeNative, nil, false), - oidc.ResponseTypeIDToken}, + oidc.ResponseTypeIDToken, + }, false, }, { "implicit flow registered http localhost web fails", - args{"http://localhost:9999/callback", + args{ + "http://localhost:9999/callback", mock.NewClientWithConfig(t, []string{"http://localhost:9999/callback"}, op.ApplicationTypeWeb, nil, false), - oidc.ResponseTypeIDToken}, + oidc.ResponseTypeIDToken, + }, true, }, { "implicit flow registered http localhost user agent fails", - args{"http://localhost:9999/callback", + args{ + "http://localhost:9999/callback", mock.NewClientWithConfig(t, []string{"http://localhost:9999/callback"}, op.ApplicationTypeUserAgent, nil, false), - oidc.ResponseTypeIDToken}, + oidc.ResponseTypeIDToken, + }, true, }, { "implicit flow http non localhost fails", - args{"http://registered.com/callback", + args{ + "http://registered.com/callback", mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeNative, nil, false), - oidc.ResponseTypeIDToken}, + oidc.ResponseTypeIDToken, + }, true, }, { "implicit flow custom fails", - args{"custom://callback", + args{ + "custom://callback", mock.NewClientWithConfig(t, []string{"custom://callback"}, op.ApplicationTypeNative, nil, false), - oidc.ResponseTypeIDToken}, + oidc.ResponseTypeIDToken, + }, false, }, { "implicit flow dev mode http ok", - args{"http://registered.com/callback", + args{ + "http://registered.com/callback", mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, true), - oidc.ResponseTypeIDToken}, + oidc.ResponseTypeIDToken, + }, false, }, } @@ -647,20 +701,26 @@ func TestValidateAuthReqResponseType(t *testing.T) { }{ { "empty response type", - args{"", - mock.NewClientWithConfig(t, nil, op.ApplicationTypeNative, []oidc.ResponseType{oidc.ResponseTypeCode}, true)}, + args{ + "", + mock.NewClientWithConfig(t, nil, op.ApplicationTypeNative, []oidc.ResponseType{oidc.ResponseTypeCode}, true), + }, true, }, { "response type missing in client config", - args{oidc.ResponseTypeIDToken, - mock.NewClientWithConfig(t, nil, op.ApplicationTypeNative, []oidc.ResponseType{oidc.ResponseTypeCode}, true)}, + args{ + oidc.ResponseTypeIDToken, + mock.NewClientWithConfig(t, nil, op.ApplicationTypeNative, []oidc.ResponseType{oidc.ResponseTypeCode}, true), + }, true, }, { "valid response type", - args{oidc.ResponseTypeCode, - mock.NewClientWithConfig(t, nil, op.ApplicationTypeNative, []oidc.ResponseType{oidc.ResponseTypeCode}, true)}, + args{ + oidc.ResponseTypeCode, + mock.NewClientWithConfig(t, nil, op.ApplicationTypeNative, []oidc.ResponseType{oidc.ResponseTypeCode}, true), + }, false, }, } diff --git a/pkg/op/config_test.go b/pkg/op/config_test.go index 5029df8..9ff75f1 100644 --- a/pkg/op/config_test.go +++ b/pkg/op/config_test.go @@ -60,7 +60,7 @@ func TestValidateIssuer(t *testing.T) { true, }, } - //ensure env is not set + // ensure env is not set //nolint:errcheck os.Unsetenv(OidcDevMode) for _, tt := range tests { diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index c06f9a2..100bfc8 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -56,7 +56,7 @@ var DefaultSupportedScopes = []string{ } func Scopes(c Configuration) []string { - return DefaultSupportedScopes //TODO: config + return DefaultSupportedScopes // TODO: config } func ResponseTypes(c Configuration) []string { @@ -64,7 +64,7 @@ func ResponseTypes(c Configuration) []string { string(oidc.ResponseTypeCode), string(oidc.ResponseTypeIDTokenOnly), string(oidc.ResponseTypeIDToken), - } //TODO: ok for now, check later if dynamic needed + } // TODO: ok for now, check later if dynamic needed } func GrantTypes(c Configuration) []oidc.GrantType { @@ -88,7 +88,7 @@ func GrantTypes(c Configuration) []oidc.GrantType { } func SupportedClaims(c Configuration) []string { - return []string{ //TODO: config + return []string{ // TODO: config "sub", "aud", "exp", @@ -121,7 +121,7 @@ func SigAlgorithms(s Signer) []string { } func SubjectTypes(c Configuration) []string { - return []string{"public"} //TODO: config + return []string{"public"} // TODO: config } func AuthMethodsTokenEndpoint(c Configuration) []oidc.AuthMethod { diff --git a/pkg/op/endpoint.go b/pkg/op/endpoint.go index 21907f4..b1e1507 100644 --- a/pkg/op/endpoint.go +++ b/pkg/op/endpoint.go @@ -27,7 +27,7 @@ func (e Endpoint) Absolute(host string) string { } func (e Endpoint) Validate() error { - return nil //TODO: + return nil // TODO: } func absoluteEndpoint(host, endpoint string) string { diff --git a/pkg/op/endpoint_test.go b/pkg/op/endpoint_test.go index 96472b2..7c8d1ce 100644 --- a/pkg/op/endpoint_test.go +++ b/pkg/op/endpoint_test.go @@ -87,7 +87,7 @@ func TestEndpoint_Absolute(t *testing.T) { } } -//TODO: impl test +// TODO: impl test func TestEndpoint_Validate(t *testing.T) { tests := []struct { name string diff --git a/pkg/op/mock/authorizer.mock.impl.go b/pkg/op/mock/authorizer.mock.impl.go index 6df6115..d4f29d5 100644 --- a/pkg/op/mock/authorizer.mock.impl.go +++ b/pkg/op/mock/authorizer.mock.impl.go @@ -68,6 +68,7 @@ type Verifier struct{} func (v *Verifier) Verify(ctx context.Context, accessToken, idToken string) (*oidc.IDTokenClaims, error) { return nil, nil } + func (v *Verifier) VerifyIDToken(ctx context.Context, idToken string) (*oidc.IDTokenClaims, error) { return nil, nil } diff --git a/pkg/op/mock/client.go b/pkg/op/mock/client.go index 7d559ae..3b16e5e 100644 --- a/pkg/op/mock/client.go +++ b/pkg/op/mock/client.go @@ -20,7 +20,8 @@ func NewClientExpectAny(t *testing.T, appType op.ApplicationType) op.Client { "https://registered.com/callback", "http://registered.com/callback", "http://localhost:9999/callback", - "custom://callback"}) + "custom://callback", + }) m.EXPECT().ApplicationType().AnyTimes().Return(appType) m.EXPECT().LoginURL(gomock.Any()).AnyTimes().DoAndReturn( func(id string) string { diff --git a/pkg/op/mock/storage.mock.impl.go b/pkg/op/mock/storage.mock.impl.go index 5b74a50..946cee0 100644 --- a/pkg/op/mock/storage.mock.impl.go +++ b/pkg/op/mock/storage.mock.impl.go @@ -44,6 +44,7 @@ func NewMockStorageSigningKeyInvalid(t *testing.T) op.Storage { ExpectSigningKeyInvalid(m) return m } + func NewMockStorageSigningKey(t *testing.T) op.Storage { m := NewStorage(t) ExpectSigningKey(m) @@ -120,6 +121,7 @@ func (c *ConfClient) RedirectURIs() []string { "custom://callback", } } + func (c *ConfClient) PostLogoutRedirectURIs() []string { return []string{} } @@ -143,34 +145,43 @@ func (c *ConfClient) GetID() string { func (c *ConfClient) AccessTokenLifetime() time.Duration { return 5 * time.Minute } + func (c *ConfClient) IDTokenLifetime() time.Duration { return 5 * time.Minute } + func (c *ConfClient) AccessTokenType() op.AccessTokenType { return c.accessTokenType } + func (c *ConfClient) ResponseTypes() []oidc.ResponseType { return c.responseTypes } + func (c *ConfClient) GrantTypes() []oidc.GrantType { return c.grantTypes } + func (c *ConfClient) DevMode() bool { return c.devMode } + func (c *ConfClient) AllowedScopes() []string { return nil } + func (c *ConfClient) RestrictAdditionalIdTokenScopes() func(scopes []string) []string { return func(scopes []string) []string { return scopes } } + func (c *ConfClient) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { return func(scopes []string) []string { return scopes } } + func (c *ConfClient) IsScopeAllowed(scope string) bool { return false } diff --git a/pkg/op/op.go b/pkg/op/op.go index 659464f..db35a87 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -29,17 +29,15 @@ const ( defaultKeysEndpoint = "keys" ) -var ( - DefaultEndpoints = &endpoints{ - Authorization: NewEndpoint(defaultAuthorizationEndpoint), - Token: NewEndpoint(defaultTokenEndpoint), - Introspection: NewEndpoint(defaultIntrospectEndpoint), - Userinfo: NewEndpoint(defaultUserinfoEndpoint), - Revocation: NewEndpoint(defaultRevocationEndpoint), - EndSession: NewEndpoint(defaultEndSessionEndpoint), - JwksURI: NewEndpoint(defaultKeysEndpoint), - } -) +var DefaultEndpoints = &endpoints{ + Authorization: NewEndpoint(defaultAuthorizationEndpoint), + Token: NewEndpoint(defaultTokenEndpoint), + Introspection: NewEndpoint(defaultIntrospectEndpoint), + Userinfo: NewEndpoint(defaultUserinfoEndpoint), + Revocation: NewEndpoint(defaultRevocationEndpoint), + EndSession: NewEndpoint(defaultEndSessionEndpoint), + JwksURI: NewEndpoint(defaultKeysEndpoint), +} type OpenIDProvider interface { Configuration @@ -83,7 +81,7 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router return router } -//AuthCallbackURL builds the url for the redirect (with the requestID) after a successful login +// AuthCallbackURL builds the url for the redirect (with the requestID) after a successful login func AuthCallbackURL(o OpenIDProvider) func(string) string { return func(requestID string) string { return o.AuthorizationEndpoint().Absolute(o.Issuer()) + authCallbackPathSuffix + "?id=" + requestID @@ -117,8 +115,8 @@ type endpoints struct { JwksURI Endpoint } -//NewOpenIDProvider creates a provider. The provider provides (with HttpHandler()) -//a http.Router that handles a suite of endpoints (some paths can be overridden): +// NewOpenIDProvider creates a provider. The provider provides (with HttpHandler()) +// a http.Router that handles a suite of endpoints (some paths can be overridden): // /healthz // /ready // /.well-known/openid-configuration @@ -130,10 +128,10 @@ type endpoints struct { // /revoke // /end_session // /keys -//This does not include login. Login is handled with a redirect that includes the -//request ID. The redirect for logins is specified per-client by Client.LoginURL(). -//Successful logins should mark the request as authorized and redirect back to to -//op.AuthCallbackURL(provider) which is probably /callback. On the redirect back +// This does not include login. Login is handled with a redirect that includes the +// request ID. The redirect for logins is specified per-client by Client.LoginURL(). +// Successful logins should mark the request as authorized and redirect back to to +// op.AuthCallbackURL(provider) which is probably /callback. On the redirect back // to the AuthCallbackURL, the request id should be passed as the "id" parameter. func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opOpts ...Option) (OpenIDProvider, error) { err := ValidateIssuer(config.Issuer) @@ -354,8 +352,8 @@ type openIDKeySet struct { Storage } -//VerifySignature implements the oidc.KeySet interface -//providing an implementation for the keys stored in the OP Storage interface +// VerifySignature implements the oidc.KeySet interface +// providing an implementation for the keys stored in the OP Storage interface func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { keySet, err := o.Storage.GetKeySet(ctx) if err != nil { diff --git a/pkg/op/probes.go b/pkg/op/probes.go index f328951..7b80fb4 100644 --- a/pkg/op/probes.go +++ b/pkg/op/probes.go @@ -39,6 +39,7 @@ func ReadySigner(s Signer) ProbesFn { return s.Health(ctx) } } + func ReadyStorage(s Storage) ProbesFn { return func(ctx context.Context) error { if s == nil { diff --git a/pkg/op/token_client_credentials.go b/pkg/op/token_client_credentials.go index 2248654..3787667 100644 --- a/pkg/op/token_client_credentials.go +++ b/pkg/op/token_client_credentials.go @@ -9,8 +9,8 @@ import ( "github.com/zitadel/oidc/pkg/oidc" ) -//ClientCredentialsExchange handles the OAuth 2.0 client_credentials grant, including -//parsing, validating, authorizing the client and finally returning a token +// ClientCredentialsExchange handles the OAuth 2.0 client_credentials grant, including +// parsing, validating, authorizing the client and finally returning a token func ClientCredentialsExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { request, err := ParseClientCredentialsRequest(r, exchanger.Decoder()) if err != nil { @@ -32,7 +32,7 @@ func ClientCredentialsExchange(w http.ResponseWriter, r *http.Request, exchanger httphelper.MarshalJSON(w, resp) } -//ParseClientCredentialsRequest parsed the http request into a oidc.ClientCredentialsRequest +// ParseClientCredentialsRequest parsed the http request into a oidc.ClientCredentialsRequest func ParseClientCredentialsRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.ClientCredentialsRequest, error) { err := r.ParseForm() if err != nil { @@ -63,8 +63,8 @@ func ParseClientCredentialsRequest(r *http.Request, decoder httphelper.Decoder) return request, nil } -//ValidateClientCredentialsRequest validates the refresh_token request parameters including authorization check of the client -//and returns the data representing the original auth request corresponding to the refresh_token +// ValidateClientCredentialsRequest validates the refresh_token request parameters including authorization check of the client +// and returns the data representing the original auth request corresponding to the refresh_token func ValidateClientCredentialsRequest(ctx context.Context, request *oidc.ClientCredentialsRequest, exchanger Exchanger) (TokenRequest, Client, error) { storage, ok := exchanger.Storage().(ClientCredentialsStorage) if !ok { diff --git a/pkg/op/token_code.go b/pkg/op/token_code.go index 185fad8..ec48233 100644 --- a/pkg/op/token_code.go +++ b/pkg/op/token_code.go @@ -8,8 +8,8 @@ import ( "github.com/zitadel/oidc/pkg/oidc" ) -//CodeExchange handles the OAuth 2.0 authorization_code grant, including -//parsing, validating, authorizing the client and finally exchanging the code for tokens +// CodeExchange handles the OAuth 2.0 authorization_code grant, including +// parsing, validating, authorizing the client and finally exchanging the code for tokens func CodeExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { tokenReq, err := ParseAccessTokenRequest(r, exchanger.Decoder()) if err != nil { @@ -32,7 +32,7 @@ func CodeExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { httphelper.MarshalJSON(w, resp) } -//ParseAccessTokenRequest parsed the http request into a oidc.AccessTokenRequest +// ParseAccessTokenRequest parsed the http request into a oidc.AccessTokenRequest func ParseAccessTokenRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.AccessTokenRequest, error) { request := new(oidc.AccessTokenRequest) err := ParseAuthenticatedTokenRequest(r, decoder, request) @@ -42,8 +42,8 @@ func ParseAccessTokenRequest(r *http.Request, decoder httphelper.Decoder) (*oidc return request, nil } -//ValidateAccessTokenRequest validates the token request parameters including authorization check of the client -//and returns the previous created auth request corresponding to the auth code +// ValidateAccessTokenRequest validates the token request parameters including authorization check of the client +// and returns the previous created auth request corresponding to the auth code func ValidateAccessTokenRequest(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (AuthRequest, Client, error) { authReq, client, err := AuthorizeCodeClient(ctx, tokenReq, exchanger) if err != nil { @@ -61,8 +61,8 @@ func ValidateAccessTokenRequest(ctx context.Context, tokenReq *oidc.AccessTokenR return authReq, client, nil } -//AuthorizeCodeClient checks the authorization of the client and that the used method was the one previously registered. -//It than returns the auth request corresponding to the auth code +// AuthorizeCodeClient checks the authorization of the client and that the used method was the one previously registered. +// It than returns the auth request corresponding to the auth code func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (request AuthRequest, client Client, err error) { if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion { jwtExchanger, ok := exchanger.(JWTAuthorizationGrantExchanger) @@ -102,7 +102,7 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, return request, client, err } -//AuthRequestByCode returns the AuthRequest previously created from Storage corresponding to the auth code or an error +// AuthRequestByCode returns the AuthRequest previously created from Storage corresponding to the auth code or an error func AuthRequestByCode(ctx context.Context, storage Storage, code string) (AuthRequest, error) { authReq, err := storage.AuthRequestByCode(ctx, code) if err != nil { diff --git a/pkg/op/token_exchange.go b/pkg/op/token_exchange.go index 501f6e5..7bb6e42 100644 --- a/pkg/op/token_exchange.go +++ b/pkg/op/token_exchange.go @@ -5,7 +5,7 @@ import ( "net/http" ) -//TokenExchange will handle the OAuth 2.0 token exchange grant ("urn:ietf:params:oauth:grant-type:token-exchange") +// TokenExchange will handle the OAuth 2.0 token exchange grant ("urn:ietf:params:oauth:grant-type:token-exchange") func TokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { RequestError(w, r, errors.New("unimplemented")) } diff --git a/pkg/op/token_jwt_profile.go b/pkg/op/token_jwt_profile.go index 0fccfe7..eb21517 100644 --- a/pkg/op/token_jwt_profile.go +++ b/pkg/op/token_jwt_profile.go @@ -14,7 +14,7 @@ type JWTAuthorizationGrantExchanger interface { JWTProfileVerifier() JWTProfileVerifier } -//JWTProfile handles the OAuth 2.0 JWT Profile Authorization Grant https://tools.ietf.org/html/rfc7523#section-2.1 +// JWTProfile handles the OAuth 2.0 JWT Profile Authorization Grant https://tools.ietf.org/html/rfc7523#section-2.1 func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizationGrantExchanger) { profileRequest, err := ParseJWTProfileGrantRequest(r, exchanger.Decoder()) if err != nil { @@ -53,7 +53,7 @@ func ParseJWTProfileGrantRequest(r *http.Request, decoder httphelper.Decoder) (* return tokenReq, nil } -//CreateJWTTokenResponse creates +// CreateJWTTokenResponse creates func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator) (*oidc.AccessTokenResponse, error) { id, exp, err := creator.Storage().CreateAccessToken(ctx, tokenRequest) if err != nil { @@ -71,7 +71,7 @@ func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, crea }, nil } -//ParseJWTProfileRequest has been renamed to ParseJWTProfileGrantRequest +// ParseJWTProfileRequest has been renamed to ParseJWTProfileGrantRequest // //deprecated: use ParseJWTProfileGrantRequest func ParseJWTProfileRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.JWTProfileGrantRequest, error) { diff --git a/pkg/op/token_refresh.go b/pkg/op/token_refresh.go index 5558a1d..7251eeb 100644 --- a/pkg/op/token_refresh.go +++ b/pkg/op/token_refresh.go @@ -21,8 +21,8 @@ type RefreshTokenRequest interface { SetCurrentScopes(scopes []string) } -//RefreshTokenExchange handles the OAuth 2.0 refresh_token grant, including -//parsing, validating, authorizing the client and finally exchanging the refresh_token for new tokens +// RefreshTokenExchange handles the OAuth 2.0 refresh_token grant, including +// parsing, validating, authorizing the client and finally exchanging the refresh_token for new tokens func RefreshTokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { tokenReq, err := ParseRefreshTokenRequest(r, exchanger.Decoder()) if err != nil { @@ -41,7 +41,7 @@ func RefreshTokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exch httphelper.MarshalJSON(w, resp) } -//ParseRefreshTokenRequest parsed the http request into a oidc.RefreshTokenRequest +// ParseRefreshTokenRequest parsed the http request into a oidc.RefreshTokenRequest func ParseRefreshTokenRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.RefreshTokenRequest, error) { request := new(oidc.RefreshTokenRequest) err := ParseAuthenticatedTokenRequest(r, decoder, request) @@ -51,8 +51,8 @@ func ParseRefreshTokenRequest(r *http.Request, decoder httphelper.Decoder) (*oid return request, nil } -//ValidateRefreshTokenRequest validates the refresh_token request parameters including authorization check of the client -//and returns the data representing the original auth request corresponding to the refresh_token +// ValidateRefreshTokenRequest validates the refresh_token request parameters including authorization check of the client +// and returns the data representing the original auth request corresponding to the refresh_token func ValidateRefreshTokenRequest(ctx context.Context, tokenReq *oidc.RefreshTokenRequest, exchanger Exchanger) (RefreshTokenRequest, Client, error) { if tokenReq.RefreshToken == "" { return nil, nil, oidc.ErrInvalidRequest().WithDescription("refresh_token missing") @@ -70,9 +70,9 @@ func ValidateRefreshTokenRequest(ctx context.Context, tokenReq *oidc.RefreshToke return request, client, nil } -//ValidateRefreshTokenScopes validates that the requested scope is a subset of the original auth request scope -//it will set the requested scopes as current scopes onto RefreshTokenRequest -//if empty the original scopes will be used +// ValidateRefreshTokenScopes validates that the requested scope is a subset of the original auth request scope +// it will set the requested scopes as current scopes onto RefreshTokenRequest +// if empty the original scopes will be used func ValidateRefreshTokenScopes(requestedScopes []string, authRequest RefreshTokenRequest) error { if len(requestedScopes) == 0 { return nil @@ -86,8 +86,8 @@ func ValidateRefreshTokenScopes(requestedScopes []string, authRequest RefreshTok return nil } -//AuthorizeRefreshClient checks the authorization of the client and that the used method was the one previously registered. -//It than returns the data representing the original auth request corresponding to the refresh_token +// AuthorizeRefreshClient checks the authorization of the client and that the used method was the one previously registered. +// It than returns the data representing the original auth request corresponding to the refresh_token func AuthorizeRefreshClient(ctx context.Context, tokenReq *oidc.RefreshTokenRequest, exchanger Exchanger) (request RefreshTokenRequest, client Client, err error) { if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion { jwtExchanger, ok := exchanger.(JWTAuthorizationGrantExchanger) @@ -128,8 +128,8 @@ func AuthorizeRefreshClient(ctx context.Context, tokenReq *oidc.RefreshTokenRequ return request, client, err } -//RefreshTokenRequestByRefreshToken returns the RefreshTokenRequest (data representing the original auth request) -//corresponding to the refresh_token from Storage or an error +// RefreshTokenRequestByRefreshToken returns the RefreshTokenRequest (data representing the original auth request) +// corresponding to the refresh_token from Storage or an error func RefreshTokenRequestByRefreshToken(ctx context.Context, storage Storage, refreshToken string) (RefreshTokenRequest, error) { request, err := storage.TokenRequestByRefreshToken(ctx, refreshToken) if err != nil { diff --git a/pkg/op/token_request.go b/pkg/op/token_request.go index dc8d118..6ccd489 100644 --- a/pkg/op/token_request.go +++ b/pkg/op/token_request.go @@ -29,7 +29,7 @@ func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Reque } } -//Exchange performs a token exchange appropriate for the grant type +// Exchange performs a token exchange appropriate for the grant type func Exchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { grantType := r.FormValue("grant_type") switch grantType { @@ -63,15 +63,15 @@ func Exchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { RequestError(w, r, oidc.ErrUnsupportedGrantType().WithDescription("%s not supported", grantType)) } -//AuthenticatedTokenRequest is a helper interface for ParseAuthenticatedTokenRequest -//it is implemented by oidc.AuthRequest and oidc.RefreshTokenRequest +// AuthenticatedTokenRequest is a helper interface for ParseAuthenticatedTokenRequest +// it is implemented by oidc.AuthRequest and oidc.RefreshTokenRequest type AuthenticatedTokenRequest interface { SetClientID(string) SetClientSecret(string) } -//ParseAuthenticatedTokenRequest parses the client_id and client_secret from the HTTP request from either -//HTTP Basic Auth header or form body and sets them into the provided authenticatedTokenRequest interface +// ParseAuthenticatedTokenRequest parses the client_id and client_secret from the HTTP request from either +// HTTP Basic Auth header or form body and sets them into the provided authenticatedTokenRequest interface func ParseAuthenticatedTokenRequest(r *http.Request, decoder httphelper.Decoder, request AuthenticatedTokenRequest) error { err := r.ParseForm() if err != nil { @@ -98,7 +98,7 @@ func ParseAuthenticatedTokenRequest(r *http.Request, decoder httphelper.Decoder, return nil } -//AuthorizeClientIDSecret authorizes a client by validating the client_id and client_secret (Basic Auth and POST) +// AuthorizeClientIDSecret authorizes a client by validating the client_id and client_secret (Basic Auth and POST) func AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string, storage Storage) error { err := storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret) if err != nil { @@ -107,8 +107,8 @@ func AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string, return nil } -//AuthorizeCodeChallenge authorizes a client by validating the code_verifier against the previously sent -//code_challenge of the auth request (PKCE) +// AuthorizeCodeChallenge authorizes a client by validating the code_verifier against the previously sent +// code_challenge of the auth request (PKCE) func AuthorizeCodeChallenge(tokenReq *oidc.AccessTokenRequest, challenge *oidc.CodeChallenge) error { if tokenReq.CodeVerifier == "" { return oidc.ErrInvalidRequest().WithDescription("code_challenge required") @@ -119,8 +119,8 @@ func AuthorizeCodeChallenge(tokenReq *oidc.AccessTokenRequest, challenge *oidc.C return nil } -//AuthorizePrivateJWTKey authorizes a client by validating the client_assertion's signature with a previously -//registered public key (JWT Profile) +// AuthorizePrivateJWTKey authorizes a client by validating the client_assertion's signature with a previously +// registered public key (JWT Profile) func AuthorizePrivateJWTKey(ctx context.Context, clientAssertion string, exchanger JWTAuthorizationGrantExchanger) (Client, error) { jwtReq, err := VerifyJWTAssertion(ctx, clientAssertion, exchanger.JWTProfileVerifier()) if err != nil { @@ -136,7 +136,7 @@ func AuthorizePrivateJWTKey(ctx context.Context, clientAssertion string, exchang return client, nil } -//ValidateGrantType ensures that the requested grant_type is allowed by the Client +// ValidateGrantType ensures that the requested grant_type is allowed by the Client func ValidateGrantType(client Client, grantType oidc.GrantType) bool { if client == nil { return false diff --git a/pkg/op/token_revocation.go b/pkg/op/token_revocation.go index b4ae266..375f7fc 100644 --- a/pkg/op/token_revocation.go +++ b/pkg/op/token_revocation.go @@ -54,9 +54,9 @@ func ParseTokenRevocationRequest(r *http.Request, revoker Revoker) (token, token } req := new(struct { oidc.RevocationRequest - oidc.ClientAssertionParams //for auth_method private_key_jwt - ClientID string `schema:"client_id"` //for auth_method none and post - ClientSecret string `schema:"client_secret"` //for auth_method post + oidc.ClientAssertionParams // for auth_method private_key_jwt + ClientID string `schema:"client_id"` // for auth_method none and post + ClientSecret string `schema:"client_secret"` // for auth_method post }) err = revoker.Decoder().Decode(req, r.Form) if err != nil { diff --git a/pkg/op/verifier_access_token.go b/pkg/op/verifier_access_token.go index d2c0c80..1729c23 100644 --- a/pkg/op/verifier_access_token.go +++ b/pkg/op/verifier_access_token.go @@ -23,27 +23,27 @@ type accessTokenVerifier struct { keySet oidc.KeySet } -//Issuer implements oidc.Verifier interface +// Issuer implements oidc.Verifier interface func (i *accessTokenVerifier) Issuer() string { return i.issuer } -//MaxAgeIAT implements oidc.Verifier interface +// MaxAgeIAT implements oidc.Verifier interface func (i *accessTokenVerifier) MaxAgeIAT() time.Duration { return i.maxAgeIAT } -//Offset implements oidc.Verifier interface +// Offset implements oidc.Verifier interface func (i *accessTokenVerifier) Offset() time.Duration { return i.offset } -//SupportedSignAlgs implements AccessTokenVerifier interface +// SupportedSignAlgs implements AccessTokenVerifier interface func (i *accessTokenVerifier) SupportedSignAlgs() []string { return i.supportedSignAlgs } -//KeySet implements AccessTokenVerifier interface +// KeySet implements AccessTokenVerifier interface func (i *accessTokenVerifier) KeySet() oidc.KeySet { return i.keySet } @@ -67,7 +67,7 @@ func NewAccessTokenVerifier(issuer string, keySet oidc.KeySet, opts ...AccessTok return verifier } -//VerifyAccessToken validates the access token (issuer, signature and expiration) +// VerifyAccessToken validates the access token (issuer, signature and expiration) func VerifyAccessToken(ctx context.Context, token string, v AccessTokenVerifier) (oidc.AccessTokenClaims, error) { claims := oidc.EmptyAccessTokenClaims() diff --git a/pkg/op/verifier_id_token_hint.go b/pkg/op/verifier_id_token_hint.go index bd9ff8e..e0372ee 100644 --- a/pkg/op/verifier_id_token_hint.go +++ b/pkg/op/verifier_id_token_hint.go @@ -61,7 +61,7 @@ func NewIDTokenHintVerifier(issuer string, keySet oidc.KeySet) IDTokenHintVerifi return verifier } -//VerifyIDTokenHint validates the id token according to +// VerifyIDTokenHint validates the id token according to //https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation func VerifyIDTokenHint(ctx context.Context, token string, v IDTokenHintVerifier) (oidc.IDTokenClaims, error) { claims := oidc.EmptyIDTokenClaims() diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index 90dfc11..0215e84 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -25,7 +25,7 @@ type jwtProfileVerifier struct { offset time.Duration } -//NewJWTProfileVerifier creates a oidc.Verifier for JWT Profile assertions (authorization grant and client authentication) +// NewJWTProfileVerifier creates a oidc.Verifier for JWT Profile assertions (authorization grant and client authentication) func NewJWTProfileVerifier(storage jwtProfileKeyStorage, issuer string, maxAgeIAT, offset time.Duration, opts ...JWTProfileVerifierOption) JWTProfileVerifier { j := &jwtProfileVerifier{ storage: storage, @@ -70,9 +70,9 @@ func (v *jwtProfileVerifier) CheckSubject(request *oidc.JWTTokenRequest) error { return v.subjectCheck(request) } -//VerifyJWTAssertion verifies the assertion string from JWT Profile (authorization grant and client authentication) +// VerifyJWTAssertion verifies the assertion string from JWT Profile (authorization grant and client authentication) // -//checks audience, exp, iat, signature and that issuer and sub are the same +// checks audience, exp, iat, signature and that issuer and sub are the same func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerifier) (*oidc.JWTTokenRequest, error) { request := new(oidc.JWTTokenRequest) payload, err := oidc.ParseToken(assertion, request) @@ -119,7 +119,7 @@ type jwtProfileKeySet struct { clientID string } -//VerifySignature implements oidc.KeySet by getting the public key from Storage implementation +// VerifySignature implements oidc.KeySet by getting the public key from Storage implementation func (k *jwtProfileKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) { keyID, _ := oidc.GetKeyIDAndAlg(jws) key, err := k.storage.GetKeyByIDAndUserID(ctx, keyID, k.clientID) From 01021e71a0cbdc757b4b5f9bbc8d4c5179b364b0 Mon Sep 17 00:00:00 2001 From: mv-kan <98518570+mv-kan@users.noreply.github.com> Date: Wed, 5 Oct 2022 09:36:06 +0200 Subject: [PATCH 138/502] chore(example): fix listener usage in app example (#224) --- example/client/app/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/client/app/app.go b/example/client/app/app.go index c2c8c3f..34c89d2 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -93,5 +93,5 @@ func main() { lis := fmt.Sprintf("127.0.0.1:%s", port) logrus.Infof("listening on http://%s/", lis) - logrus.Fatal(http.ListenAndServe("127.0.0.1:"+port, nil)) + logrus.Fatal(http.ListenAndServe(lis, nil)) } From 9f71e4c92404c093517b68daf4be3fca06b32e68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Oct 2022 22:38:10 +0200 Subject: [PATCH 139/502] chore(deps): bump golang.org/x/text from 0.3.7 to 0.3.8 (#228) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.7 to 0.3.8. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.3.7...v0.3.8) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 +--- go.sum | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 616d0e0..f6385ef 100644 --- a/go.mod +++ b/go.mod @@ -15,10 +15,8 @@ require ( github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.0 github.com/zitadel/logging v0.3.4 - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect - golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 - golang.org/x/text v0.3.7 + golang.org/x/text v0.3.8 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/square/go-jose.v2 v2.6.0 ) diff --git a/go.sum b/go.sum index 68116ea..c3f393a 100644 --- a/go.sum +++ b/go.sum @@ -146,6 +146,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zitadel/logging v0.3.4 h1:9hZsTjMMTE3X2LUi0xcF9Q9EdLo+FAezeu52ireBbHM= github.com/zitadel/logging v0.3.4/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -191,6 +192,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -219,8 +221,8 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -237,6 +239,7 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -265,21 +268,23 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220207234003-57398862261d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 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= @@ -324,6 +329,7 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 3a7b2e8eb5bfc1c17b0e04cae19df5c1f4bde4e5 Mon Sep 17 00:00:00 2001 From: Weny Xu Date: Mon, 17 Oct 2022 15:06:41 +0900 Subject: [PATCH 140/502] docs(README.md): fix typos --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d32cc3..49d7290 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Check the `/example` folder where example code for different scenarios is locate # oidc discovery http://localhost:9998/.well-known/openid-configuration go run github.com/zitadel/oidc/example/server # start oidc web client -CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/example/client/app +CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998 SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/example/client/app ``` - open http://localhost:9999/login in your browser From 4bc4bfffe8eec5db040ea272afebee26c271e274 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Sun, 16 Oct 2022 23:07:19 -0700 Subject: [PATCH 141/502] add op.AllAuthMethods (#233) --- pkg/oidc/discovery.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go index 4a817e8..fbc417b 100644 --- a/pkg/oidc/discovery.go +++ b/pkg/oidc/discovery.go @@ -157,3 +157,7 @@ const ( AuthMethodNone AuthMethod = "none" AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt" ) + +var AllAuthMethods = []AuthMethod{ + AuthMethodBasic, AuthMethodPost, AuthMethodNone, AuthMethodPrivateKeyJWT, +} From 4ac692bfd83b832eeb9f59d24ae000dfb117eb13 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Mon, 17 Oct 2022 09:13:54 +0200 Subject: [PATCH 142/502] chore: house cleaning of the caos name and update sec (#232) * chore: house cleaning of the caos name and update sec * some typos * make fix non breakable * Update SECURITY.md Co-authored-by: Livio Spring * Update SECURITY.md Co-authored-by: Livio Spring Co-authored-by: Livio Spring --- README.md | 2 +- SECURITY.md | 11 ++++++----- example/client/api/api.go | 2 +- pkg/op/config.go | 12 ++++++++++-- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 49d7290..21a8198 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Versions that also build are marked with :warning:. ## Why another library -As of 2020 there are not a lot of `OIDC` library's in `Go` which can handle server and client implementations. CAOS is strongly committed to the general field of IAM (Identity and Access Management) and as such, we need solid frameworks to implement services. +As of 2020 there are not a lot of `OIDC` library's in `Go` which can handle server and client implementations. ZITADEL is strongly committed to the general field of IAM (Identity and Access Management) and as such, we need solid frameworks to implement services. ### Goals diff --git a/SECURITY.md b/SECURITY.md index dca11f3..934426a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ # Security Policy -At CAOS we are extremely grateful for security aware people that disclose vulnerabilities to us and the open source community. All reports will be investigated by our team. +At ZITADEL we are extremely grateful for security aware people that disclose vulnerabilities to us and the open source community. All reports will be investigated by our team. ## Supported Versions @@ -8,12 +8,13 @@ After the initial Release the following version support will apply | Version | Supported | | ------- | ------------------ | -| 1.x.x | :white_check_mark: (not yet available) | | 0.x.x | :x: | +| 1.x.x | :white_check_mark: | +| 2.x.x | :white_check_mark: (not released) | ## Reporting a vulnerability -To file a incident, please disclose by email to security@caos.ch with the security details. +To file a incident, please disclose by email to security@zitadel.com with the security details. At the moment GPG encryption is no yet supported, however you may sign your message at will. @@ -35,8 +36,8 @@ TBD ## Public Disclosure -All accepted and mitigated vulnerabilitys will be published on the [Github Security Page](https://github.com/zitadel/oidc/security/advisories) +All accepted and mitigated vulnerabilities will be published on the [Github Security Page](https://github.com/zitadel/oidc/security/advisories) ### Timing -We think it is crucial to publish advisories `ASAP` as mitigations are ready. But due to the unknown nature of the discloures the time frame can range from 7 to 90 days. +We think it is crucial to publish advisories `ASAP` as mitigations are ready. But due to the unknown nature of the disclosures the time frame can range from 7 to 90 days. diff --git a/example/client/api/api.go b/example/client/api/api.go index 2220554..0ab669d 100644 --- a/example/client/api/api.go +++ b/example/client/api/api.go @@ -62,7 +62,7 @@ func main() { // protected url which needs an active token and checks if the response of the introspect endpoint // contains a requested claim with the required (string) value - // e.g. /protected/username/livio@caos.ch + // e.g. /protected/username/livio@zitadel.example router.HandleFunc(protectedClaimURL, func(w http.ResponseWriter, r *http.Request) { ok, token := checkToken(w, r) if !ok { diff --git a/pkg/op/config.go b/pkg/op/config.go index 8882964..82cbb47 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -8,7 +8,11 @@ import ( "golang.org/x/text/language" ) -const OidcDevMode = "CAOS_OIDC_DEV" +const ( + OidcDevMode = "ZITADEL_OIDC_DEV" + // deprecated: use OidcDevMode (ZITADEL_OIDC_DEV=true) + devMode = "CAOS_OIDC_DEV" +) type Configuration interface { Issuer() string @@ -63,7 +67,11 @@ func ValidateIssuer(issuer string) error { func devLocalAllowed(url *url.URL) bool { _, b := os.LookupEnv(OidcDevMode) if !b { - return b + // check the old / current env var as well + _, b = os.LookupEnv(devMode) + if !b { + return b + } } return url.Scheme == "http" } From 0596d83b33d947936300411c65a4c5e73e344d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anthony=20Qu=C3=A9r=C3=A9?= <47711333+Anthony-Jhoiro@users.noreply.github.com> Date: Thu, 3 Nov 2022 11:11:15 +0100 Subject: [PATCH 143/502] doc: fix zitadel doc uri in the README (#239) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 21a8198..fd82a78 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ For your convenience you can find the relevant standards linked below. - [OAuth 2.0 Token Exchange](https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-19) - [OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens](https://tools.ietf.org/html/draft-ietf-oauth-mtls-17) - [JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants](https://tools.ietf.org/html/rfc7523) -- [OIDC/OAuth Flow in Zitadel (using this library)](https://docs.zitadel.com/docs/guides/authentication/login-users) +- [OIDC/OAuth Flow in Zitadel (using this library)](https://docs.zitadel.com/docs/guides/integrate/login-users) ## Supported Go Versions From 89d1c90bf26bed5e23f57f92a8640010131ed425 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Mon, 14 Nov 2022 07:58:36 -0800 Subject: [PATCH 144/502] fix: WithPath on NewCookieHandler set domain instead! (#240) --- pkg/http/cookie.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/http/cookie.go b/pkg/http/cookie.go index 4949b77..1ebc9e2 100644 --- a/pkg/http/cookie.go +++ b/pkg/http/cookie.go @@ -59,7 +59,7 @@ func WithDomain(domain string) CookieHandlerOpt { func WithPath(path string) CookieHandlerOpt { return func(c *CookieHandler) { - c.domain = path + c.path = path } } From 1aa75ec9533f7cb1f00533f5cf3d7dac7619242e Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Mon, 14 Nov 2022 07:59:33 -0800 Subject: [PATCH 145/502] feat: allow id token hint verifier to specify algs (#229) --- pkg/op/op.go | 10 +++++++++- pkg/op/verifier_id_token_hint.go | 13 ++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/pkg/op/op.go b/pkg/op/op.go index db35a87..59f1897 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -190,6 +190,7 @@ type openidProvider struct { interceptors []HttpInterceptor timer <-chan time.Time accessTokenVerifierOpts []AccessTokenVerifierOpt + idTokenHintVerifierOpts []IDTokenHintVerifierOpt } func (o *openidProvider) Issuer() string { @@ -299,7 +300,7 @@ func (o *openidProvider) Encoder() httphelper.Encoder { func (o *openidProvider) IDTokenHintVerifier() IDTokenHintVerifier { if o.idTokenHintVerifier == nil { - o.idTokenHintVerifier = NewIDTokenHintVerifier(o.Issuer(), o.openIDKeySet()) + o.idTokenHintVerifier = NewIDTokenHintVerifier(o.Issuer(), o.openIDKeySet(), o.idTokenHintVerifierOpts...) } return o.idTokenHintVerifier } @@ -465,6 +466,13 @@ func WithAccessTokenVerifierOpts(opts ...AccessTokenVerifierOpt) Option { } } +func WithIDTokenHintVerifierOpts(opts ...IDTokenHintVerifierOpt) Option { + return func(o *openidProvider) error { + o.idTokenHintVerifierOpts = opts + return nil + } +} + func buildInterceptor(interceptors ...HttpInterceptor) func(http.HandlerFunc) http.Handler { return func(handlerFunc http.HandlerFunc) http.Handler { handler := handlerFuncToHandler(handlerFunc) diff --git a/pkg/op/verifier_id_token_hint.go b/pkg/op/verifier_id_token_hint.go index e0372ee..d36bbd8 100644 --- a/pkg/op/verifier_id_token_hint.go +++ b/pkg/op/verifier_id_token_hint.go @@ -53,11 +53,22 @@ func (i *idTokenHintVerifier) MaxAge() time.Duration { return i.maxAge } -func NewIDTokenHintVerifier(issuer string, keySet oidc.KeySet) IDTokenHintVerifier { +type IDTokenHintVerifierOpt func(*idTokenHintVerifier) + +func WithSupportedIDTokenHintSigningAlgorithms(algs ...string) IDTokenHintVerifierOpt { + return func(verifier *idTokenHintVerifier) { + verifier.supportedSignAlgs = algs + } +} + +func NewIDTokenHintVerifier(issuer string, keySet oidc.KeySet, opts ...IDTokenHintVerifierOpt) IDTokenHintVerifier { verifier := &idTokenHintVerifier{ issuer: issuer, keySet: keySet, } + for _, opt := range opts { + opt(verifier) + } return verifier } From a314c1483fc866ccf5850ccf2b06a8a57d900609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Utku=20=C3=96zdemir?= Date: Mon, 14 Nov 2022 16:59:56 +0100 Subject: [PATCH 146/502] fix: allow http schema for redirect url for native apps in dev mode (#242) --- pkg/op/auth_request.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 9dc07d2..d8c960e 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -314,6 +314,9 @@ func validateAuthReqRedirectURINative(client Client, uri string, responseType oi parsedURL, isLoopback := HTTPLoopbackOrLocalhost(uri) isCustomSchema := !strings.HasPrefix(uri, "http://") if str.Contains(client.RedirectURIs(), uri) { + if client.DevMode() { + return nil + } if isLoopback || isCustomSchema { return nil } From 4e302ca4da3f579d0900750ca35495ccfa9da610 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Mon, 14 Nov 2022 08:00:27 -0800 Subject: [PATCH 147/502] bugfix: access token verifier opts was not used (#237) --- pkg/op/op.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/op/op.go b/pkg/op/op.go index 59f1897..8a0dc46 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -314,7 +314,7 @@ func (o *openidProvider) JWTProfileVerifier() JWTProfileVerifier { func (o *openidProvider) AccessTokenVerifier() AccessTokenVerifier { if o.accessTokenVerifier == nil { - o.accessTokenVerifier = NewAccessTokenVerifier(o.Issuer(), o.openIDKeySet()) + o.accessTokenVerifier = NewAccessTokenVerifier(o.Issuer(), o.openIDKeySet(), o.accessTokenVerifierOpts...) } return o.accessTokenVerifier } From bd47b5ddc4eeb1ff8fb42ca2ed5379620eb9cdbc Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Mon, 14 Nov 2022 08:01:19 -0800 Subject: [PATCH 148/502] feat: support EndSession with RelyingParty client (#230) * feat: support EndSession with RelyingPart client * do not error if OP does not provide a redirect * undo that last change, but noice error returns from EndSession * ioutil.ReadAll, for now --- pkg/client/client.go | 38 ++++++++++++++++++++++++++++++++++ pkg/client/rp/relying_party.go | 11 ++++++++++ 2 files changed, 49 insertions(+) diff --git a/pkg/client/client.go b/pkg/client/client.go index d6d27f7..e286a00 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -1,7 +1,11 @@ package client import ( + "errors" + "fmt" + "io/ioutil" "net/http" + "net/url" "reflect" "strings" "time" @@ -71,6 +75,40 @@ func callTokenEndpoint(request interface{}, authFn interface{}, caller TokenEndp }, nil } +type EndSessionCaller interface { + GetEndSessionEndpoint() string + HttpClient() *http.Client +} + +func CallEndSessionEndpoint(request interface{}, authFn interface{}, caller EndSessionCaller) (*url.URL, error) { + req, err := httphelper.FormRequest(caller.GetEndSessionEndpoint(), request, Encoder, authFn) + if err != nil { + return nil, err + } + client := caller.HttpClient() + client.CheckRedirect = func(_ *http.Request, _ []*http.Request) error { + return http.ErrUseLastResponse + } + resp, err := client.Do(req) + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 400 { + // TODO: switch to io.ReadAll when go1.15 support is retired + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return nil, fmt.Errorf("EndSession failure, %d status code: %s", resp.StatusCode, string(body)) + } + location, err := resp.Location() + if err != nil { + if errors.Is(err, http.ErrNoLocation) { + return nil, nil + } + return nil, err + } + return location, nil +} + func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) { privateKey, err := crypto.BytesToPrivateKey(key) if err != nil { diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index cb271e7..39c2fe7 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "errors" "net/http" + "net/url" "strings" "time" @@ -573,3 +574,13 @@ func RefreshAccessToken(rp RelyingParty, refreshToken, clientAssertion, clientAs } return client.CallTokenEndpoint(request, tokenEndpointCaller{RelyingParty: rp}) } + +func EndSession(rp RelyingParty, idToken, optionalRedirectURI, optionalState string) (*url.URL, error) { + request := oidc.EndSessionRequest{ + IdTokenHint: idToken, + ClientID: rp.OAuthConfig().ClientID, + PostLogoutRedirectURI: optionalRedirectURI, + State: optionalState, + } + return client.CallEndSessionEndpoint(request, nil, rp) +} From 0e30c387910587698bc4996a782aeaaa6908cb2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Nov 2022 17:02:22 +0100 Subject: [PATCH 149/502] chore(deps): bump golang.org/x/text from 0.3.8 to 0.4.0 (#234) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.8 to 0.4.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.3.8...v0.4.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f6385ef..5369de9 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/stretchr/testify v1.8.0 github.com/zitadel/logging v0.3.4 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 - golang.org/x/text v0.3.8 + golang.org/x/text v0.4.0 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/square/go-jose.v2 v2.6.0 ) diff --git a/go.sum b/go.sum index c3f393a..a02f522 100644 --- a/go.sum +++ b/go.sum @@ -283,8 +283,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 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= From 0847a5985a5ffa034bf58a32405a61587f9e2de4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Nov 2022 17:02:43 +0100 Subject: [PATCH 150/502] chore(deps): bump github.com/stretchr/testify from 1.8.0 to 1.8.1 (#236) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.0 to 1.8.1. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.8.0...v1.8.1) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 5369de9..572b3e8 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/gorilla/securecookie v1.1.1 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/sirupsen/logrus v1.9.0 - github.com/stretchr/testify v1.8.0 + github.com/stretchr/testify v1.8.1 github.com/zitadel/logging v0.3.4 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 golang.org/x/text v0.4.0 diff --git a/go.sum b/go.sum index a02f522..f02e038 100644 --- a/go.sum +++ b/go.sum @@ -136,12 +136,14 @@ github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0 github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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= From 39852f602192dbc1a6f42a6f0af01d25fb92e309 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Mon, 14 Nov 2022 22:35:16 -0800 Subject: [PATCH 151/502] feat: add rp.RevokeToken (#231) * feat: add rp.RevokeToken * add missing lines after conflict resolving Co-authored-by: Livio Spring --- NEXT_RELEASE.md | 6 +++++ example/server/storage/storage.go | 6 ++--- pkg/client/client.go | 41 +++++++++++++++++++++++++++++++ pkg/client/rp/relying_party.go | 28 +++++++++++++++++++++ pkg/op/storage.go | 7 +++++- pkg/op/token_revocation.go | 5 +++- 6 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 NEXT_RELEASE.md diff --git a/NEXT_RELEASE.md b/NEXT_RELEASE.md new file mode 100644 index 0000000..d4f9e71 --- /dev/null +++ b/NEXT_RELEASE.md @@ -0,0 +1,6 @@ + +# Backwards-incompatible changes to be made in the next major release + +- Add `rp/RelyingParty.GetRevokeEndpoint` +- Rename `op/OpStorage.GetKeyByIDAndUserID` to `op/OpStorage.GetKeyByIDAndClientID` + diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index 7b9d413..ae445aa 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -255,11 +255,11 @@ func (s *Storage) TerminateSession(ctx context.Context, userID string, clientID // RevokeToken implements the op.Storage interface // it will be called after parsing and validation of the token revocation request -func (s *Storage) RevokeToken(ctx context.Context, token string, userID string, clientID string) *oidc.Error { +func (s *Storage) RevokeToken(ctx context.Context, tokenIDOrToken string, userID string, clientID string) *oidc.Error { // a single token was requested to be removed s.lock.Lock() defer s.lock.Unlock() - accessToken, ok := s.tokens[token] + accessToken, ok := s.tokens[tokenIDOrToken] // tokenID if ok { if accessToken.ApplicationID != clientID { return oidc.ErrInvalidClient().WithDescription("token was not issued for this client") @@ -269,7 +269,7 @@ func (s *Storage) RevokeToken(ctx context.Context, token string, userID string, delete(s.tokens, accessToken.ID) return nil } - refreshToken, ok := s.refreshTokens[token] + refreshToken, ok := s.refreshTokens[tokenIDOrToken] // token if !ok { // if the token is neither an access nor a refresh token, just ignore it, the expected behaviour of // being not valid (anymore) is achieved diff --git a/pkg/client/client.go b/pkg/client/client.go index e286a00..344e26b 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -109,6 +109,47 @@ func CallEndSessionEndpoint(request interface{}, authFn interface{}, caller EndS return location, nil } +type RevokeCaller interface { + GetRevokeEndpoint() string + HttpClient() *http.Client +} + +type RevokeRequest struct { + Token string `schema:"token"` + TokenTypeHint string `schema:"token_type_hint"` + ClientID string `schema:"client_id"` + ClientSecret string `schema:"client_secret"` +} + +func CallRevokeEndpoint(request interface{}, authFn interface{}, caller RevokeCaller) error { + req, err := httphelper.FormRequest(caller.GetRevokeEndpoint(), request, Encoder, authFn) + if err != nil { + return err + } + client := caller.HttpClient() + client.CheckRedirect = func(_ *http.Request, _ []*http.Request) error { + return http.ErrUseLastResponse + } + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + // According to RFC7009 in section 2.2: + // "The content of the response body is ignored by the client as all + // necessary information is conveyed in the response code." + if resp.StatusCode != 200 { + // TODO: switch to io.ReadAll when go1.15 support is retired + body, err := ioutil.ReadAll(resp.Body) + if err == nil { + return fmt.Errorf("revoke returned status %d and text: %s", resp.StatusCode, string(body)) + } else { + return fmt.Errorf("revoke returned status %d", resp.StatusCode) + } + } + return nil +} + func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) { privateKey, err := crypto.BytesToPrivateKey(key) if err != nil { diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 39c2fe7..86b65da 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "errors" + "fmt" "net/http" "net/url" "strings" @@ -52,6 +53,9 @@ type RelyingParty interface { // GetEndSessionEndpoint returns the endpoint to sign out on a IDP GetEndSessionEndpoint() string + // GetRevokeEndpoint returns the endpoint to revoke a specific token + // "GetRevokeEndpoint() string" will be added in a future release + // UserinfoEndpoint returns the userinfo UserinfoEndpoint() string @@ -121,6 +125,10 @@ func (rp *relyingParty) GetEndSessionEndpoint() string { return rp.endpoints.EndSessionURL } +func (rp *relyingParty) GetRevokeEndpoint() string { + return rp.endpoints.RevokeURL +} + func (rp *relyingParty) IDTokenVerifier() IDTokenVerifier { if rp.idTokenVerifier == nil { rp.idTokenVerifier = NewIDTokenVerifier(rp.issuer, rp.oauthConfig.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...) @@ -491,6 +499,7 @@ type Endpoints struct { UserinfoURL string JKWsURL string EndSessionURL string + RevokeURL string } func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints { @@ -504,6 +513,7 @@ func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints { UserinfoURL: discoveryConfig.UserinfoEndpoint, JKWsURL: discoveryConfig.JwksURI, EndSessionURL: discoveryConfig.EndSessionEndpoint, + RevokeURL: discoveryConfig.RevocationEndpoint, } } @@ -584,3 +594,21 @@ func EndSession(rp RelyingParty, idToken, optionalRedirectURI, optionalState str } return client.CallEndSessionEndpoint(request, nil, rp) } + +// RevokeToken requires a RelyingParty that is also a client.RevokeCaller. The RelyingParty +// returned by NewRelyingPartyOIDC() meets that criteria, but the one returned by +// NewRelyingPartyOAuth() does not. +// +// tokenTypeHint should be either "id_token" or "refresh_token". +func RevokeToken(rp RelyingParty, token string, tokenTypeHint string) error { + request := client.RevokeRequest{ + Token: token, + TokenTypeHint: tokenTypeHint, + ClientID: rp.OAuthConfig().ClientID, + ClientSecret: rp.OAuthConfig().ClientSecret, + } + if rc, ok := rp.(client.RevokeCaller); ok && rc.GetRevokeEndpoint() != "" { + return client.CallRevokeEndpoint(request, nil, rc) + } + return fmt.Errorf("RelyingParty does not support RevokeCaller") +} diff --git a/pkg/op/storage.go b/pkg/op/storage.go index da536b9..2b3c93f 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -39,7 +39,12 @@ type AuthStorage interface { TokenRequestByRefreshToken(ctx context.Context, refreshTokenID string) (RefreshTokenRequest, error) TerminateSession(ctx context.Context, userID string, clientID string) error - RevokeToken(ctx context.Context, tokenID string, userID string, clientID string) *oidc.Error + + // RevokeToken should revoke a token. In the situation that the original request was to + // revoke an access token, then tokenOrTokenID will be a tokenID and userID will be set + // but if the original request was for a refresh token, then userID will be empty and + // tokenOrTokenID will be the refresh token, not its ID. + RevokeToken(ctx context.Context, tokenOrTokenID string, userID string, clientID string) *oidc.Error GetSigningKey(context.Context, chan<- jose.SigningKey) GetKeySet(context.Context) (*jose.JSONWebKeySet, error) diff --git a/pkg/op/token_revocation.go b/pkg/op/token_revocation.go index 375f7fc..db5eea8 100644 --- a/pkg/op/token_revocation.go +++ b/pkg/op/token_revocation.go @@ -113,8 +113,11 @@ func ParseTokenRevocationRequest(r *http.Request, revoker Revoker) (token, token func RevocationRequestError(w http.ResponseWriter, r *http.Request, err error) { e := oidc.DefaultToServerError(err, err.Error()) status := http.StatusBadRequest - if e.ErrorType == oidc.InvalidClient { + switch e.ErrorType { + case oidc.InvalidClient: status = 401 + case oidc.ServerError: + status = 500 } httphelper.MarshalJSONWithStatus(w, e, status) } From 74e18233921b6f8cccfd8467c2aeddbf4ca473aa Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Thu, 17 Nov 2022 22:29:25 -0800 Subject: [PATCH 152/502] chore: add an RP/OP integration test (#238) * rp/op integration test do not error if OP does not provide a redirect working, but with debugging clean up, remove debugging support go1.15 attempt to fix coverage calculation * Update pkg/client/rp/integration_test.go Co-authored-by: Livio Spring Co-authored-by: Livio Spring --- .github/workflows/release.yml | 2 +- example/server/storage/storage.go | 8 +- go.mod | 1 + go.sum | 2 + pkg/client/rp/integration_test.go | 268 ++++++++++++++++++++++++++++++ 5 files changed, 276 insertions(+), 5 deletions(-) create mode 100644 pkg/client/rp/integration_test.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8912cd4..87f94ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: uses: actions/setup-go@v3 with: go-version: ${{ matrix.go }} - - run: go test -race -v -coverprofile=profile.cov ./pkg/... + - run: go test -race -v -coverprofile=profile.cov -coverpkg=github.com/zitadel/oidc/... ./pkg/... - uses: codecov/codecov-action@v3.1.1 with: file: ./profile.cov diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index ae445aa..130822e 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -150,7 +150,7 @@ func (s *Storage) AuthRequestByCode(ctx context.Context, code string) (op.AuthRe // SaveAuthCode implements the op.Storage interface // it will be called after the authentication has been successful and before redirecting the user agent to the redirect_uri -//(in an authorization code flow) +// (in an authorization code flow) func (s *Storage) SaveAuthCode(ctx context.Context, id string, code string) error { // for this example we'll just save the authRequestID to the code s.lock.Lock() @@ -161,8 +161,8 @@ func (s *Storage) SaveAuthCode(ctx context.Context, id string, code string) erro // DeleteAuthRequest implements the op.Storage interface // it will be called after creating the token response (id and access tokens) for a valid -//- authentication request (in an implicit flow) -//- token request (in an authorization code flow) +// - authentication request (in an implicit flow) +// - token request (in an authorization code flow) func (s *Storage) DeleteAuthRequest(ctx context.Context, id string) error { // you can simply delete all reference to the auth request s.lock.Lock() @@ -247,7 +247,6 @@ func (s *Storage) TerminateSession(ctx context.Context, userID string, clientID if token.ApplicationID == clientID && token.Subject == userID { delete(s.tokens, token.ID) delete(s.refreshTokens, token.RefreshTokenID) - return nil } } return nil @@ -509,6 +508,7 @@ func (s *Storage) renewRefreshToken(currentRefreshToken string) (string, string, // creates a new refresh token based on the current one token := uuid.NewString() refreshToken.Token = token + refreshToken.ID = token s.refreshTokens[token] = refreshToken return token, refreshToken.ID, nil } diff --git a/go.mod b/go.mod index 572b3e8..2ced4e1 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/gorilla/mux v1.8.0 github.com/gorilla/schema v1.2.0 github.com/gorilla/securecookie v1.1.1 + github.com/jeremija/gosubmit v0.2.7 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.1 diff --git a/go.sum b/go.sum index f02e038..1d9b954 100644 --- a/go.sum +++ b/go.sum @@ -118,6 +118,8 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc= +github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= diff --git a/pkg/client/rp/integration_test.go b/pkg/client/rp/integration_test.go new file mode 100644 index 0000000..e08e2eb --- /dev/null +++ b/pkg/client/rp/integration_test.go @@ -0,0 +1,268 @@ +package rp_test + +import ( + "bytes" + "context" + "io/ioutil" + "math/rand" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "net/url" + "os" + "strconv" + "testing" + "time" + + "github.com/zitadel/oidc/example/server/exampleop" + "github.com/zitadel/oidc/example/server/storage" + + "github.com/jeremija/gosubmit" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/oidc/pkg/client/rp" + httphelper "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/pkg/oidc" +) + +func TestRelyingPartySession(t *testing.T) { + t.Log("------- start example OP ------") + ctx := context.Background() + exampleStorage := storage.NewStorage(storage.NewUserStore()) + var dh deferredHandler + opServer := httptest.NewServer(&dh) + defer opServer.Close() + t.Logf("auth server at %s", opServer.URL) + dh.Handler = exampleop.SetupServer(ctx, opServer.URL, exampleStorage) + + targetURL := "http://local-site" + localURL, err := url.Parse(targetURL + "/login?requestID=1234") + require.NoError(t, err, "local url") + + seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano())) + clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25) + client := storage.WebClient(clientID, "secret", targetURL) + storage.RegisterClients(client) + + jar, err := cookiejar.New(nil) + require.NoError(t, err, "create cookie jar") + httpClient := &http.Client{ + Timeout: time.Second * 5, + CheckRedirect: func(_ *http.Request, _ []*http.Request) error { + return http.ErrUseLastResponse + }, + Jar: jar, + } + + t.Log("------- create RP ------") + key := []byte("test1234test1234") + cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure()) + provider, err := rp.NewRelyingPartyOIDC( + opServer.URL, + clientID, + "secret", + targetURL, + []string{"openid", "email", "profile", "offline_access"}, + rp.WithPKCE(cookieHandler), + rp.WithVerifierOpts( + rp.WithIssuedAtOffset(5*time.Second), + rp.WithSupportedSigningAlgorithms("RS256", "RS384", "RS512", "ES256", "ES384", "ES512"), + ), + ) + + t.Log("------- get redirect from local client (rp) to OP ------") + state := "state-" + strconv.FormatInt(seed.Int63(), 25) + capturedW := httptest.NewRecorder() + get := httptest.NewRequest("GET", localURL.String(), nil) + rp.AuthURLHandler(func() string { return state }, provider)(capturedW, get) + + defer func() { + if t.Failed() { + t.Log("response body (redirect from RP to OP)", capturedW.Body.String()) + } + }() + require.GreaterOrEqual(t, capturedW.Code, 200, "captured response code") + require.Less(t, capturedW.Code, 400, "captured response code") + + //nolint:bodyclose + resp := capturedW.Result() + jar.SetCookies(localURL, resp.Cookies()) + + startAuthURL, err := resp.Location() + require.NoError(t, err, "get redirect") + assert.NotEmpty(t, startAuthURL, "login url") + t.Log("Starting auth at", startAuthURL) + + t.Log("------- get redirect to OP to login page ------") + loginPageURL := getRedirect(t, "get redirect to login page", httpClient, startAuthURL) + t.Log("login page URL", loginPageURL) + + t.Log("------- get login form ------") + form := getForm(t, "get login form", httpClient, loginPageURL) + t.Log("login form (unfilled)", string(form)) + defer func() { + if t.Failed() { + t.Logf("login form (unfilled): %s", string(form)) + } + }() + + t.Log("------- post to login form, get redirect to OP ------") + postLoginRedirectURL := fillForm(t, "fill login form", httpClient, form, loginPageURL, + gosubmit.Set("username", "test-user"), + gosubmit.Set("password", "verysecure")) + t.Logf("Get redirect from %s", postLoginRedirectURL) + + t.Log("------- redirect from OP back to RP ------") + codeBearingURL := getRedirect(t, "get redirect with code", httpClient, postLoginRedirectURL) + t.Logf("Redirect with code %s", codeBearingURL) + + t.Log("------- exchange code for tokens ------") + capturedW = httptest.NewRecorder() + get = httptest.NewRequest("GET", codeBearingURL.String(), nil) + for _, cookie := range jar.Cookies(codeBearingURL) { + get.Header["Cookie"] = append(get.Header["Cookie"], cookie.String()) + t.Logf("setting cookie %s", cookie) + } + + var accessToken, refreshToken, idToken, email string + redirect := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) { + require.NotNil(t, tokens, "tokens") + require.NotNil(t, info, "info") + t.Log("access token", tokens.AccessToken) + t.Log("refresh token", tokens.RefreshToken) + t.Log("id token", tokens.IDToken) + t.Log("email", info.GetEmail()) + + accessToken = tokens.AccessToken + refreshToken = tokens.RefreshToken + idToken = tokens.IDToken + email = info.GetEmail() + http.Redirect(w, r, targetURL, 302) + } + rp.CodeExchangeHandler(rp.UserinfoCallback(redirect), provider)(capturedW, get) + + defer func() { + if t.Failed() { + t.Log("token exchange response body", capturedW.Body.String()) + require.GreaterOrEqual(t, capturedW.Code, 200, "captured response code") + } + }() + require.Less(t, capturedW.Code, 400, "token exchange response code") + require.Less(t, capturedW.Code, 400, "token exchange response code") + + //nolint:bodyclose + resp = capturedW.Result() + + authorizedURL, err := resp.Location() + require.NoError(t, err, "get fully-authorizied redirect location") + require.Equal(t, targetURL, authorizedURL.String(), "fully-authorizied redirect location") + + require.NotEmpty(t, idToken, "id token") + assert.NotEmpty(t, refreshToken, "refresh token") + assert.NotEmpty(t, accessToken, "access token") + assert.NotEmpty(t, email, "email") + + t.Log("------- refresh tokens ------") + + newTokens, err := rp.RefreshAccessToken(provider, refreshToken, "", "") + require.NoError(t, err, "refresh token") + assert.NotNil(t, newTokens, "access token") + t.Logf("new access token %s", newTokens.AccessToken) + t.Logf("new refresh token %s", newTokens.RefreshToken) + t.Logf("new token type %s", newTokens.TokenType) + t.Logf("new expiry %s", newTokens.Expiry.Format(time.RFC3339)) + require.NotEmpty(t, newTokens.AccessToken, "new accessToken") + + t.Log("------ end session (logout) ------") + + newLoc, err := rp.EndSession(provider, idToken, "", "") + require.NoError(t, err, "logout") + if newLoc != nil { + t.Logf("redirect to %s", newLoc) + } else { + t.Logf("no redirect") + } + + t.Log("------ attempt refresh again (should fail) ------") + t.Log("trying original refresh token", refreshToken) + _, err = rp.RefreshAccessToken(provider, refreshToken, "", "") + assert.Errorf(t, err, "refresh with original") + if newTokens.RefreshToken != "" { + t.Log("trying replacement refresh token", newTokens.RefreshToken) + _, err = rp.RefreshAccessToken(provider, newTokens.RefreshToken, "", "") + assert.Errorf(t, err, "refresh with replacement") + } +} + +type deferredHandler struct { + http.Handler +} + +func getRedirect(t *testing.T, desc string, httpClient *http.Client, uri *url.URL) *url.URL { + req := &http.Request{ + Method: "GET", + URL: uri, + Header: make(http.Header), + } + resp, err := httpClient.Do(req) + require.NoError(t, err, "GET "+uri.String()) + + defer func() { + if t.Failed() { + // TODO: switch to io.ReadAll when go1.15 support is dropped + body, _ := ioutil.ReadAll(resp.Body) + t.Logf("%s: GET %s: body: %s", desc, uri, string(body)) + } + }() + + //nolint:errcheck + defer resp.Body.Close() + redirect, err := resp.Location() + require.NoErrorf(t, err, "%s: get redirect %s", desc, uri) + require.NotEmptyf(t, redirect, "%s: get redirect %s", desc, uri) + return redirect +} + +func getForm(t *testing.T, desc string, httpClient *http.Client, uri *url.URL) []byte { + req := &http.Request{ + Method: "GET", + URL: uri, + Header: make(http.Header), + } + resp, err := httpClient.Do(req) + require.NoErrorf(t, err, "%s: GET %s", desc, uri) + //nolint:errcheck + defer resp.Body.Close() + // TODO: switch to io.ReadAll when go1.15 support is dropped + body, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err, "%s: read GET %s", desc, uri) + return body +} + +func fillForm(t *testing.T, desc string, httpClient *http.Client, body []byte, uri *url.URL, opts ...gosubmit.Option) *url.URL { + // TODO: switch to io.NopCloser when go1.15 support is dropped + req := gosubmit.ParseWithURL(ioutil.NopCloser(bytes.NewReader(body)), uri.String()).FirstForm().Testing(t).NewTestRequest( + append([]gosubmit.Option{gosubmit.AutoFill()}, opts...)..., + ) + if req.URL.Scheme == "" { + req.URL = uri + t.Log("request lost it's proto..., adding back... request now", req.URL) + } + req.RequestURI = "" // bug in gosubmit? + resp, err := httpClient.Do(req) + require.NoErrorf(t, err, "%s: POST %s", desc, uri) + + //nolint:errcheck + defer resp.Body.Close() + defer func() { + if t.Failed() { + // TODO: switch to io.ReadAll when go1.15 support is dropped + body, _ := ioutil.ReadAll(resp.Body) + t.Logf("%s: GET %s: body: %s", desc, uri, string(body)) + } + }() + + redirect, err := resp.Location() + require.NoErrorf(t, err, "%s: redirect for POST %s", desc, uri) + return redirect +} From 356dd89ae4fad3e95ba118ec3ee51f0b42c97448 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Mon, 21 Nov 2022 17:41:56 +0100 Subject: [PATCH 153/502] chore: fix broken codecov default branch (#245) * chore: fix broken codecov default branch * update codecov badge --- .codecov/codecov.yml | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.codecov/codecov.yml b/.codecov/codecov.yml index b4394d2..0f17a71 100644 --- a/.codecov/codecov.yml +++ b/.codecov/codecov.yml @@ -1,4 +1,5 @@ codecov: + branch: main notify: require_ci_to_pass: yes coverage: diff --git a/README.md b/README.md index fd82a78..8e22700 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![license](https://badgen.net/github/license/zitadel/oidc/)](https://github.com/zitadel/oidc/blob/master/LICENSE) [![release](https://badgen.net/github/release/zitadel/oidc/stable)](https://github.com/zitadel/oidc/releases) [![Go Report Card](https://goreportcard.com/badge/github.com/zitadel/oidc)](https://goreportcard.com/report/github.com/zitadel/oidc) -[![codecov](https://codecov.io/gh/zitadel/oidc/branch/master/graph/badge.svg)](https://codecov.io/gh/zitadel/oidc) +[![codecov](https://codecov.io/gh/zitadel/oidc/branch/main/graph/badge.svg)](https://codecov.io/gh/zitadel/oidc) ![openid_certified](https://cloud.githubusercontent.com/assets/1454075/7611268/4d19de32-f97b-11e4-895b-31b2455a7ca6.png) From c0f3ef8a66b5bc4da6aaf41b2e6134ad478ae5ad Mon Sep 17 00:00:00 2001 From: Michael Holtermann Date: Thu, 24 Nov 2022 15:30:16 +0100 Subject: [PATCH 154/502] Add folders to Basic Overview --- README.md | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 8e22700..e43c208 100644 --- a/README.md +++ b/README.md @@ -23,18 +23,18 @@ Whenever possible we tried to reuse / extend existing packages like `OAuth2 for The most important packages of the library:
 /pkg
-    /client     clients using the OP for retrieving, exchanging and verifying tokens       
-        /rp     definition and implementation of an OIDC Relying Party (client)
-        /rs     definition and implementation of an OAuth Resource Server (API)
-    /op         definition and implementation of an OIDC OpenID Provider (server)
-    /oidc       definitions shared by clients and server
+    /client            clients using the OP for retrieving, exchanging and verifying tokens       
+        /rp            definition and implementation of an OIDC Relying Party (client)
+        /rs            definition and implementation of an OAuth Resource Server (API)
+    /op                definition and implementation of an OIDC OpenID Provider (server)
+    /oidc              definitions shared by clients and server
 
 /example
-    /api        example of an api / resource server implementation using token introspection
-    /app        web app / RP demonstrating authorization code flow using various authentication methods (code, PKCE, JWT profile)
-    /github     example of the extended OAuth2 library, providing an HTTP client with a reuse token source
-    /service    demonstration of JWT Profile Authorization Grant
-    /server     example of an OpenID Provider implementation including some very basic login UI
+    /client/api        example of an api / resource server implementation using token introspection
+    /client/app        web app / RP demonstrating authorization code flow using various authentication methods (code, PKCE, JWT profile)
+    /client/github     example of the extended OAuth2 library, providing an HTTP client with a reuse token source
+    /client/service    demonstration of JWT Profile Authorization Grant
+    /server            example of an OpenID Provider implementation including some very basic login UI
 
## How To Use It @@ -64,7 +64,7 @@ CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998 SCOPES="openid p ## Contributors - + Screen with contributors' avatars from contrib.rocks Made with [contrib.rocks](https://contrib.rocks). @@ -114,11 +114,14 @@ We did not choose `fosite` because it implements `OAuth 2.0` on its own and does ## License -The full functionality of this library is and stays open source and free to use for everyone. Visit our [website](https://zitadel.com) and get in touch. +The full functionality of this library is and stays open source and free to use for everyone. Visit +our [website](https://zitadel.com) and get in touch. -See the exact licensing terms [here](./LICENSE) +See the exact licensing terms [here](LICENSE) -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an " +AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific +language governing permissions and limitations under the License. [^1]: https://github.com/zitadel/oidc/issues/135#issuecomment-950563892 From 46684fbe0d41d81bcd7f2115fed3bc9665e7aba6 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Tue, 6 Dec 2022 09:35:23 +0100 Subject: [PATCH 155/502] chore(codeql): update branch name --- .github/workflows/codeql-analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7b852d2..85ea2ca 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,10 +2,10 @@ name: "Code scanning - action" on: push: - branches: [master, ] + branches: [main, ] pull_request: # The branches below must be a subset of the branches above - branches: [master] + branches: [main] schedule: - cron: '0 11 * * 0' From 87a545e60bd2ceee955227df61e0dfaf031a0c94 Mon Sep 17 00:00:00 2001 From: Goran Kovacevic <36618604+goran-hc@users.noreply.github.com> Date: Tue, 6 Dec 2022 11:34:19 +0100 Subject: [PATCH 156/502] feat: add missing IntrospectionResponse getters (#251) --- pkg/oidc/introspection.go | 46 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index 3ff7c66..b7c220c 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -30,6 +30,16 @@ type IntrospectionResponse interface { SetAudience(audience []string) SetIssuer(issuer string) SetJWTID(id string) + GetScope() []string + GetClientID() string + GetTokenType() string + GetExpiration() time.Time + GetIssuedAt() time.Time + GetNotBefore() time.Time + GetSubject() string + GetAudience() []string + GetIssuer() string + GetJWTID() string } func NewIntrospectionResponse() IntrospectionResponse { @@ -144,6 +154,42 @@ func (i *introspectionResponse) GetClaims() map[string]interface{} { return i.claims } +func (i *introspectionResponse) GetScope() []string { + return []string(i.Scope) +} + +func (i *introspectionResponse) GetClientID() string { + return i.ClientID +} + +func (i *introspectionResponse) GetTokenType() string { + return i.TokenType +} + +func (i *introspectionResponse) GetExpiration() time.Time { + return time.Time(i.Expiration) +} + +func (i *introspectionResponse) GetIssuedAt() time.Time { + return time.Time(i.IssuedAt) +} + +func (i *introspectionResponse) GetNotBefore() time.Time { + return time.Time(i.NotBefore) +} + +func (i *introspectionResponse) GetAudience() []string { + return []string(i.Audience) +} + +func (i *introspectionResponse) GetIssuer() string { + return i.Issuer +} + +func (i *introspectionResponse) GetJWTID() string { + return i.JWTID +} + func (i *introspectionResponse) SetActive(active bool) { i.Active = active } From 2fd92af1f8ed09a043d8415923a6664211e652e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Dec 2022 11:34:54 +0100 Subject: [PATCH 157/502] chore(deps): bump actions/add-to-project from 0.3.0 to 0.4.0 (#249) Bumps [actions/add-to-project](https://github.com/actions/add-to-project) from 0.3.0 to 0.4.0. - [Release notes](https://github.com/actions/add-to-project/releases) - [Commits](https://github.com/actions/add-to-project/compare/v0.3.0...v0.4.0) --- updated-dependencies: - dependency-name: actions/add-to-project dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/issue.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 61f49a4..631a032 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -10,7 +10,7 @@ jobs: name: Add issue to project runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@v0.3.0 + - uses: actions/add-to-project@v0.4.0 with: # You can target a repository in a different organization # to the issue From aa7cb56f69a84ee2c91fd1beeef3282b05e28ec5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Dec 2022 11:37:56 +0100 Subject: [PATCH 158/502] chore(deps): bump golang.org/x/text from 0.4.0 to 0.5.0 (#250) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.4.0 to 0.5.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.4.0...v0.5.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2ced4e1..f6a69f4 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/stretchr/testify v1.8.1 github.com/zitadel/logging v0.3.4 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 - golang.org/x/text v0.4.0 + golang.org/x/text v0.5.0 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/square/go-jose.v2 v2.6.0 ) diff --git a/go.sum b/go.sum index 1d9b954..d40657a 100644 --- a/go.sum +++ b/go.sum @@ -287,8 +287,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 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= From 205f2c4a307fde45c6810f581e29af8e0b3d027e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Dec 2022 10:41:07 +0000 Subject: [PATCH 159/502] chore(deps): bump cycjimmy/semantic-release-action from 2 to 3 (#248) * chore(deps): bump cycjimmy/semantic-release-action from 2 to 3 Bumps [cycjimmy/semantic-release-action](https://github.com/cycjimmy/semantic-release-action) from 2 to 3. - [Release notes](https://github.com/cycjimmy/semantic-release-action/releases) - [Changelog](https://github.com/cycjimmy/semantic-release-action/blob/main/docs/CHANGELOG.md) - [Commits](https://github.com/cycjimmy/semantic-release-action/compare/v2...v3) --- updated-dependencies: - dependency-name: cycjimmy/semantic-release-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * update sem rel to work with node 16 Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Livio Spring --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 87f94ed..4366a35 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,9 +38,9 @@ jobs: - name: Source checkout uses: actions/checkout@v3 - name: Semantic Release - uses: cycjimmy/semantic-release-action@v2 + uses: cycjimmy/semantic-release-action@v3 with: dry_run: false - semantic_version: 17.0.4 + semantic_version: 18.0.1 extra_plugins: | - @semantic-release/exec@5.0.0 + @semantic-release/exec@6.0.3 From b6eea1ddda81d746136b1af1cf339328a0067e03 Mon Sep 17 00:00:00 2001 From: Fabi <38692350+hifabienne@users.noreply.github.com> Date: Thu, 29 Dec 2022 16:03:40 +0100 Subject: [PATCH 160/502] Update issue.yml --- .github/workflows/issue.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 631a032..d7a3cc9 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -1,4 +1,4 @@ -name: Add new issues to kanban project +name: Add new issues to product management project on: issues: @@ -14,5 +14,5 @@ jobs: with: # You can target a repository in a different organization # to the issue - project-url: https://github.com/orgs/zitadel/projects/1 + project-url: https://github.com/orgs/zitadel/projects/2 github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} From b031c1f29786b15a42dbbab74613d6beb29730d9 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Mon, 9 Jan 2023 10:39:11 +0100 Subject: [PATCH 161/502] fix: exchange cors library and add `X-Requested-With` to Access-Control-Request-Headers (#260) --- go.mod | 2 +- go.sum | 6 ++---- pkg/op/op.go | 34 +++++++++++++++++++++++++--------- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index f6a69f4..df1323c 100644 --- a/go.mod +++ b/go.mod @@ -7,12 +7,12 @@ require ( github.com/google/go-cmp v0.5.2 // indirect github.com/google/go-github/v31 v31.0.0 github.com/google/uuid v1.3.0 - github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 github.com/gorilla/schema v1.2.0 github.com/gorilla/securecookie v1.1.1 github.com/jeremija/gosubmit v0.2.7 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/rs/cors v1.8.3 github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.1 github.com/zitadel/logging v0.3.4 diff --git a/go.sum b/go.sum index d40657a..ec7b82e 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,6 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= -github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 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= @@ -107,8 +105,6 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= -github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= @@ -133,6 +129,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo= +github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= diff --git a/pkg/op/op.go b/pkg/op/op.go index 8a0dc46..d85dcd6 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -6,9 +6,9 @@ import ( "net/http" "time" - "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/gorilla/schema" + "github.com/rs/cors" "golang.org/x/text/language" "gopkg.in/square/go-jose.v2" @@ -55,18 +55,34 @@ type OpenIDProvider interface { type HttpInterceptor func(http.Handler) http.Handler -var allowAllOrigins = func(_ string) bool { - return true +var defaultCORSOptions = cors.Options{ + AllowCredentials: true, + AllowedHeaders: []string{ + "Origin", + "Accept", + "Accept-Language", + "Authorization", + "Content-Type", + "X-Requested-With", + }, + AllowedMethods: []string{ + http.MethodGet, + http.MethodHead, + http.MethodPost, + }, + ExposedHeaders: []string{ + "Location", + "Content-Length", + }, + AllowOriginFunc: func(_ string) bool { + return true + }, } func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router { intercept := buildInterceptor(interceptors...) router := mux.NewRouter() - router.Use(handlers.CORS( - handlers.AllowCredentials(), - handlers.AllowedHeaders([]string{"authorization", "content-type"}), - handlers.AllowedOriginValidator(allowAllOrigins), - )) + router.Use(cors.New(defaultCORSOptions).Handler) router.HandleFunc(healthEndpoint, healthHandler) router.HandleFunc(readinessEndpoint, readyHandler(o.Probes())) router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Signer())) @@ -190,7 +206,7 @@ type openidProvider struct { interceptors []HttpInterceptor timer <-chan time.Time accessTokenVerifierOpts []AccessTokenVerifierOpt - idTokenHintVerifierOpts []IDTokenHintVerifierOpt + idTokenHintVerifierOpts []IDTokenHintVerifierOpt } func (o *openidProvider) Issuer() string { From 1535ea4f6c4a4256903b6fbf644a87692ddc7e35 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Wed, 25 Jan 2023 06:22:12 +0100 Subject: [PATCH 162/502] chore(examples): improve logging and how to use (#266) --- README.md | 2 +- example/client/app/app.go | 1 + example/server/main.go | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e43c208..1be33e7 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Check the `/example` folder where example code for different scenarios is locate # start oidc op server # oidc discovery http://localhost:9998/.well-known/openid-configuration go run github.com/zitadel/oidc/example/server -# start oidc web client +# start oidc web client (in a new terminal) CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998 SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/example/client/app ``` diff --git a/example/client/app/app.go b/example/client/app/app.go index 34c89d2..b7f2868 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -93,5 +93,6 @@ func main() { lis := fmt.Sprintf("127.0.0.1:%s", port) logrus.Infof("listening on http://%s/", lis) + logrus.Info("press ctrl+c to stop") logrus.Fatal(http.ListenAndServe(lis, nil)) } diff --git a/example/server/main.go b/example/server/main.go index 37fbcb3..3cfd20d 100644 --- a/example/server/main.go +++ b/example/server/main.go @@ -24,6 +24,8 @@ func main() { Addr: ":" + port, Handler: router, } + log.Printf("server listening on http://localhost:%s/", port) + log.Println("press ctrl+c to stop") err := server.ListenAndServe() if err != nil { log.Fatal(err) From fa222c5efb331a225c931b07721c33f55cf6c2bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 3 Feb 2023 12:14:04 +0200 Subject: [PATCH 163/502] fix: nil pointer dereference on UserInfoAddress (#207) * oidc: add test case to reproduce #203 Running the tests will always result in a nil pointer dereference on UserInfoAddress. Co-authored-by: Livio Spring * fix: nil pointer dereference on UserInfoAddress userinfo.UnmarshalJSON now only sets the Address field if it was present in the json. userinfo.GetAddress will always return a non-nil value of UserInfoAddress to allow for safe chaining of Get functions. Fixes #203 --------- Co-authored-by: Livio Spring --- pkg/oidc/userinfo.go | 9 ++++++++- pkg/oidc/userinfo_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/pkg/oidc/userinfo.go b/pkg/oidc/userinfo.go index 4d524e3..c8e34d6 100644 --- a/pkg/oidc/userinfo.go +++ b/pkg/oidc/userinfo.go @@ -167,6 +167,9 @@ func (u *userinfo) IsPhoneNumberVerified() bool { } func (u *userinfo) GetAddress() UserInfoAddress { + if u.Address == nil { + return &userInfoAddress{} + } return u.Address } @@ -389,7 +392,11 @@ func (u *userinfo) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &a); err != nil { return err } - u.Address = a.Address + + if a.Address != nil { + u.Address = a.Address + } + u.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC()) if err := json.Unmarshal(data, &u.claims); err != nil { diff --git a/pkg/oidc/userinfo_test.go b/pkg/oidc/userinfo_test.go index f42ff3d..319a2fd 100644 --- a/pkg/oidc/userinfo_test.go +++ b/pkg/oidc/userinfo_test.go @@ -81,3 +81,39 @@ func TestUserInfoEmailVerifiedUnmarshal(t *testing.T) { }, uie) }) } + +// issue 203 test case. +func Test_userinfo_GetAddress_issue_203(t *testing.T) { + tests := []struct { + name string + data string + }{ + { + name: "with address", + data: `{"address":{"street_address":"Test 789\nPostfach 2"},"email":"test","email_verified":true,"name":"Test","phone_number":"0791234567","phone_number_verified":true,"private_claim":"test","sub":"test"}`, + }, + { + name: "without address", + data: `{"email":"test","email_verified":true,"name":"Test","phone_number":"0791234567","phone_number_verified":true,"private_claim":"test","sub":"test"}`, + }, + { + name: "null address", + data: `{"address":null,"email":"test","email_verified":true,"name":"Test","phone_number":"0791234567","phone_number_verified":true,"private_claim":"test","sub":"test"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + info := &userinfo{} + err := json.Unmarshal([]byte(tt.data), info) + assert.NoError(t, err) + + info.GetAddress().GetCountry() //<- used to panic + + // now shortly assure that a marshalling still produces the same as was parsed into the struct + marshal, err := json.Marshal(info) + assert.NoError(t, err) + assert.Equal(t, tt.data, string(marshal)) + }) + } +} From cdf2af6c2c3b10b3c066ae28ff43e7acd5a6e0b4 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Sun, 5 Feb 2023 23:27:57 -0800 Subject: [PATCH 164/502] feat: add CanRefreshTokenInfo to support non-JWT refresh tokens (#244) * Add an additional, optional, op.Storage interface so that refresh tokens that are not JWTs do not cause failures when they randomly, sometimes, decrypt without error ```go // CanRefreshTokenInfo is an optional additional interface that Storage can support. // Supporting CanRefreshTokenInfo is required to be able to revoke a refresh token that // does not happen to also be a JWTs work properly. type CanRefreshTokenInfo interface { // GetRefreshTokenInfo must return oidc.ErrInvalidRefreshToken when presented // with a token that is not a refresh token. GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error) } ``` * add comment suggested in code review * review feedback: return an error defined in op rather than adding a new error to oidc * move ErrInvalidRefresToken to op/storage.go --- NEXT_RELEASE.md | 1 + pkg/op/storage.go | 12 ++++++++++++ pkg/op/token_revocation.go | 28 ++++++++++++++++++++++++---- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/NEXT_RELEASE.md b/NEXT_RELEASE.md index d4f9e71..91f7f5d 100644 --- a/NEXT_RELEASE.md +++ b/NEXT_RELEASE.md @@ -3,4 +3,5 @@ - Add `rp/RelyingParty.GetRevokeEndpoint` - Rename `op/OpStorage.GetKeyByIDAndUserID` to `op/OpStorage.GetKeyByIDAndClientID` +- Add `CanRefreshTokenInfo` (`GetRefreshTokenInfo()`) to `op.Storage` diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 2b3c93f..153cd21 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -2,6 +2,7 @@ package op import ( "context" + "errors" "time" "gopkg.in/square/go-jose.v2" @@ -50,6 +51,17 @@ type AuthStorage interface { GetKeySet(context.Context) (*jose.JSONWebKeySet, error) } +// CanRefreshTokenInfo is an optional additional interface that Storage can support. +// Supporting CanRefreshTokenInfo is required to be able to (revoke) a refresh token that +// is neither an encrypted string of : nor a JWT. +type CanRefreshTokenInfo interface { + // GetRefreshTokenInfo must return ErrInvalidRefreshToken when presented + // with a token that is not a refresh token. + GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error) +} + +var ErrInvalidRefreshToken = errors.New("invalid_refresh_token") + type ClientCredentialsStorage interface { ClientCredentialsTokenRequest(ctx context.Context, clientID string, scopes []string) (TokenRequest, error) } diff --git a/pkg/op/token_revocation.go b/pkg/op/token_revocation.go index db5eea8..9dd0295 100644 --- a/pkg/op/token_revocation.go +++ b/pkg/op/token_revocation.go @@ -2,6 +2,7 @@ package op import ( "context" + "errors" "net/http" "net/url" "strings" @@ -31,14 +32,33 @@ func revocationHandler(revoker Revoker) func(http.ResponseWriter, *http.Request) } func Revoke(w http.ResponseWriter, r *http.Request, revoker Revoker) { - token, _, clientID, err := ParseTokenRevocationRequest(r, revoker) + token, tokenTypeHint, clientID, err := ParseTokenRevocationRequest(r, revoker) if err != nil { RevocationRequestError(w, r, err) return } - tokenID, subject, ok := getTokenIDAndSubjectForRevocation(r.Context(), revoker, token) - if ok { - token = tokenID + var subject string + doDecrypt := true + if canRefreshInfo, ok := revoker.Storage().(CanRefreshTokenInfo); ok && tokenTypeHint != "access_token" { + userID, tokenID, err := canRefreshInfo.GetRefreshTokenInfo(r.Context(), clientID, token) + if err != nil { + // An invalid refresh token means that we'll try other things (leaving doDecrypt==true) + if !errors.Is(err, ErrInvalidRefreshToken) { + RevocationRequestError(w, r, oidc.ErrServerError().WithParent(err)) + return + } + } else { + token = tokenID + subject = userID + doDecrypt = false + } + } + if doDecrypt { + tokenID, userID, ok := getTokenIDAndSubjectForRevocation(r.Context(), revoker, token) + if ok { + token = tokenID + subject = userID + } } if err := revoker.Storage().RevokeToken(r.Context(), token, subject, clientID); err != nil { RevocationRequestError(w, r, err) From df5a09f8139fae00b6a507f977fa515892c9c8bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Mon, 6 Feb 2023 09:29:25 +0200 Subject: [PATCH 165/502] chore: switch from iouitil to io.ReadAll (#272) removed a TODO: switch to io.ReadAll and drop go1.15 support --- .github/workflows/release.yml | 2 +- README.md | 3 +-- example/client/service/service.go | 6 +++--- go.mod | 2 +- pkg/client/client.go | 8 +++----- pkg/client/rp/integration_test.go | 10 ++++------ pkg/http/http.go | 4 ++-- 7 files changed, 15 insertions(+), 20 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4366a35..c50c741 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - go: ['1.15', '1.16', '1.17', '1.18', '1.19'] + go: ['1.16', '1.17', '1.18', '1.19'] name: Go ${{ matrix.go }} test steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index 1be33e7..74ce435 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,7 @@ Versions that also build are marked with :warning:. | Version | Supported | |---------|--------------------| -| <1.15 | :x: | -| 1.15 | :warning: | +| <1.16 | :x: | | 1.16 | :warning: | | 1.17 | :warning: | | 1.18 | :white_check_mark: | diff --git a/example/client/service/service.go b/example/client/service/service.go index 980abcd..b3819d5 100644 --- a/example/client/service/service.go +++ b/example/client/service/service.go @@ -5,7 +5,7 @@ import ( "encoding/json" "fmt" "html/template" - "io/ioutil" + "io" "net/http" "os" "strings" @@ -71,7 +71,7 @@ func main() { } defer file.Close() - key, err := ioutil.ReadAll(file) + key, err := io.ReadAll(file) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -161,7 +161,7 @@ func callExampleEndpoint(client *http.Client, testURL string) (interface{}, erro } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } diff --git a/go.mod b/go.mod index df1323c..18ae410 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/zitadel/oidc -go 1.15 +go 1.16 require ( github.com/golang/mock v1.6.0 diff --git a/pkg/client/client.go b/pkg/client/client.go index 344e26b..62f1019 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -3,7 +3,7 @@ package client import ( "errors" "fmt" - "io/ioutil" + "io" "net/http" "net/url" "reflect" @@ -92,8 +92,7 @@ func CallEndSessionEndpoint(request interface{}, authFn interface{}, caller EndS resp, err := client.Do(req) defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 400 { - // TODO: switch to io.ReadAll when go1.15 support is retired - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } @@ -139,8 +138,7 @@ func CallRevokeEndpoint(request interface{}, authFn interface{}, caller RevokeCa // "The content of the response body is ignored by the client as all // necessary information is conveyed in the response code." if resp.StatusCode != 200 { - // TODO: switch to io.ReadAll when go1.15 support is retired - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err == nil { return fmt.Errorf("revoke returned status %d and text: %s", resp.StatusCode, string(body)) } else { diff --git a/pkg/client/rp/integration_test.go b/pkg/client/rp/integration_test.go index e08e2eb..6f5f489 100644 --- a/pkg/client/rp/integration_test.go +++ b/pkg/client/rp/integration_test.go @@ -3,6 +3,7 @@ package rp_test import ( "bytes" "context" + "io" "io/ioutil" "math/rand" "net/http" @@ -209,8 +210,7 @@ func getRedirect(t *testing.T, desc string, httpClient *http.Client, uri *url.UR defer func() { if t.Failed() { - // TODO: switch to io.ReadAll when go1.15 support is dropped - body, _ := ioutil.ReadAll(resp.Body) + body, _ := io.ReadAll(resp.Body) t.Logf("%s: GET %s: body: %s", desc, uri, string(body)) } }() @@ -233,8 +233,7 @@ func getForm(t *testing.T, desc string, httpClient *http.Client, uri *url.URL) [ require.NoErrorf(t, err, "%s: GET %s", desc, uri) //nolint:errcheck defer resp.Body.Close() - // TODO: switch to io.ReadAll when go1.15 support is dropped - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) require.NoError(t, err, "%s: read GET %s", desc, uri) return body } @@ -256,8 +255,7 @@ func fillForm(t *testing.T, desc string, httpClient *http.Client, body []byte, u defer resp.Body.Close() defer func() { if t.Failed() { - // TODO: switch to io.ReadAll when go1.15 support is dropped - body, _ := ioutil.ReadAll(resp.Body) + body, _ := io.ReadAll(resp.Body) t.Logf("%s: GET %s: body: %s", desc, uri, string(body)) } }() diff --git a/pkg/http/http.go b/pkg/http/http.go index 0e97b6f..d3c5b4f 100644 --- a/pkg/http/http.go +++ b/pkg/http/http.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "log" "net/http" "net/url" @@ -60,7 +60,7 @@ func HttpRequest(client *http.Client, req *http.Request, response interface{}) e } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("unable to read response body: %v", err) } From 3a6c3543e7b8acc00d666420cdfe684c3ddd0bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Mon, 6 Feb 2023 11:35:50 +0200 Subject: [PATCH 166/502] chore: add go 1.20 support (#274) --- .github/workflows/release.yml | 2 +- README.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c50c741..d97d41a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - go: ['1.16', '1.17', '1.18', '1.19'] + go: ['1.16', '1.17', '1.18', '1.19', '1.20'] name: Go ${{ matrix.go }} test steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index 74ce435..d0356ce 100644 --- a/README.md +++ b/README.md @@ -90,8 +90,9 @@ Versions that also build are marked with :warning:. | <1.16 | :x: | | 1.16 | :warning: | | 1.17 | :warning: | -| 1.18 | :white_check_mark: | +| 1.18 | :warning: | | 1.19 | :white_check_mark: | +| 1.20 | :white_check_mark: | ## Why another library From a34d7a1630a5ce92fac54e3434802aa891531a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Mon, 6 Feb 2023 12:11:11 +0200 Subject: [PATCH 167/502] chore: add go 1.20 support (#275) --- .github/workflows/release.yml | 2 +- README.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c50c741..d97d41a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - go: ['1.16', '1.17', '1.18', '1.19'] + go: ['1.16', '1.17', '1.18', '1.19', '1.20'] name: Go ${{ matrix.go }} test steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index 74ce435..d0356ce 100644 --- a/README.md +++ b/README.md @@ -90,8 +90,9 @@ Versions that also build are marked with :warning:. | <1.16 | :x: | | 1.16 | :warning: | | 1.17 | :warning: | -| 1.18 | :white_check_mark: | +| 1.18 | :warning: | | 1.19 | :white_check_mark: | +| 1.20 | :white_check_mark: | ## Why another library From e59b9259a7ededcf070c4f68d9d65133c6b4632d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 18:35:36 +0000 Subject: [PATCH 168/502] chore(deps): bump golang.org/x/text from 0.5.0 to 0.6.0 Bumps [golang.org/x/text](https://github.com/golang/text) from 0.5.0 to 0.6.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.5.0...v0.6.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 18ae410..1895c24 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/stretchr/testify v1.8.1 github.com/zitadel/logging v0.3.4 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 - golang.org/x/text v0.5.0 + golang.org/x/text v0.6.0 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/square/go-jose.v2 v2.6.0 ) diff --git a/go.sum b/go.sum index ec7b82e..5f3ac4b 100644 --- a/go.sum +++ b/go.sum @@ -285,8 +285,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 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= From d258fc4c29722ca12f4e54a6ca2e7c58e54b1efa Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Fri, 27 Jan 2023 10:08:09 -0800 Subject: [PATCH 169/502] document lack of client caching --- pkg/op/storage.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 153cd21..28fc6a3 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -67,6 +67,8 @@ type ClientCredentialsStorage interface { } type OPStorage interface { + // GetClientByClientID loads a Client. The returned Client is never cached and is only used to + // handle the current request. GetClientByClientID(ctx context.Context, clientID string) (Client, error) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error From 1165d88c69e7aadebe9ceda31fe0ec93349607f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Thu, 9 Feb 2023 18:10:22 +0200 Subject: [PATCH 170/502] feat(op): dynamic issuer depending on request / host (#278) * feat(op): dynamic issuer depending on request / host BREAKING CHANGE: The OpenID Provider package is now able to handle multiple issuers with a single storage implementation. The issuer will be selected from the host of the request and passed into the context, where every function can read it from if necessary. This results in some fundamental changes: - `Configuration` interface: - `Issuer() string` has been changed to `IssuerFromRequest(r *http.Request) string` - `Insecure() bool` has been added - OpenIDProvider interface and dependants: - `Issuer` has been removed from Config struct - `NewOpenIDProvider` now takes an additional parameter `issuer` and returns a pointer to the public/default implementation and not an OpenIDProvider interface: `NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opOpts ...Option) (OpenIDProvider, error)` changed to `NewOpenIDProvider(ctx context.Context, issuer string, config *Config, storage Storage, opOpts ...Option) (*Provider, error)` - therefore the parameter type Option changed to the public type as well: `Option func(o *Provider) error` - `AuthCallbackURL(o OpenIDProvider) func(string) string` has been changed to `AuthCallbackURL(o OpenIDProvider) func(context.Context, string) string` - `IDTokenHintVerifier() IDTokenHintVerifier` (Authorizer, OpenIDProvider, SessionEnder interfaces), `AccessTokenVerifier() AccessTokenVerifier` (Introspector, OpenIDProvider, Revoker, UserinfoProvider interfaces) and `JWTProfileVerifier() JWTProfileVerifier` (IntrospectorJWTProfile, JWTAuthorizationGrantExchanger, OpenIDProvider, RevokerJWTProfile interfaces) now take a context.Context parameter `IDTokenHintVerifier(context.Context) IDTokenHintVerifier`, `AccessTokenVerifier(context.Context) AccessTokenVerifier` and `JWTProfileVerifier(context.Context) JWTProfileVerifier` - `OidcDevMode` (CAOS_OIDC_DEV) environment variable check has been removed, use `WithAllowInsecure()` Option - Signing: the signer is not kept in memory anymore, but created on request from the loaded key: - `Signer` interface and func `NewSigner` have been removed - `ReadySigner(s Signer) ProbesFn` has been removed - `CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfiguration` has been changed to `CreateDiscoveryConfig(r *http.Request, config Configuration, storage DiscoverStorage) *oidc.DiscoveryConfiguration` - `Storage` interface: - `GetSigningKey(context.Context, chan<- jose.SigningKey)` has been changed to `SigningKey(context.Context) (SigningKey, error)` - `KeySet(context.Context) ([]Key, error)` has been added - `GetKeySet(context.Context) (*jose.JSONWebKeySet, error)` has been changed to `KeySet(context.Context) ([]Key, error)` - `SigAlgorithms(s Signer) []string` has been changed to `SigAlgorithms(ctx context.Context, storage DiscoverStorage) []string` - KeyProvider interface: `GetKeySet(context.Context) (*jose.JSONWebKeySet, error)` has been changed to `KeySet(context.Context) ([]Key, error)` - `CreateIDToken`: the Signer parameter has been removed * move example * fix examples * fix mocks * update readme * fix examples and update usage * update go module version to v2 * build branch * fix(module): rename caos to zitadel * fix: add state in access token response (implicit flow) * fix: encode auth response correctly (when using query in redirect uri) * fix query param handling * feat: add all optional claims of the introspection response * fix: use default redirect uri when not passed * fix: exchange cors library and add `X-Requested-With` to Access-Control-Request-Headers (#261) * feat(op): add support for client credentials * fix mocks and test * feat: allow to specify token type of JWT Profile Grant * document JWTProfileTokenStorage * cleanup * rp: fix integration test test username needed to be suffixed by issuer domain * chore(deps): bump golang.org/x/text from 0.5.0 to 0.6.0 Bumps [golang.org/x/text](https://github.com/golang/text) from 0.5.0 to 0.6.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.5.0...v0.6.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * op: mock: cleanup commented code * op: remove duplicate code code duplication caused by merge conflict selections --------- Signed-off-by: dependabot[bot] Co-authored-by: Livio Amstutz Co-authored-by: adlerhurst Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .releaserc.js | 5 +- README.md | 17 +- example/client/api/api.go | 4 +- example/client/app/app.go | 6 +- example/client/github/github.go | 6 +- example/client/service/service.go | 2 +- example/doc.go | 3 +- example/server/dynamic/login.go | 113 +++++ example/server/dynamic/op.go | 138 ++++++ example/server/exampleop/login.go | 7 +- example/server/exampleop/op.go | 13 +- example/server/main.go | 15 +- example/server/storage/client.go | 12 +- example/server/storage/oidc.go | 5 +- example/server/storage/storage.go | 86 ++-- example/server/storage/storage_dynamic.go | 260 +++++++++++ example/server/storage/user.go | 6 +- go.mod | 5 +- go.sum | 13 +- pkg/client/client.go | 6 +- pkg/client/jwt_profile.go | 4 +- pkg/client/profile/jwt_profile.go | 4 +- pkg/client/rp/cli/cli.go | 6 +- pkg/client/rp/delegation.go | 6 +- pkg/client/rp/integration_test.go | 17 +- pkg/client/rp/jwks.go | 4 +- pkg/client/rp/mock/verifier.mock.go | 2 +- pkg/client/rp/relying_party.go | 12 +- pkg/client/rp/tockenexchange.go | 2 +- pkg/client/rp/verifier.go | 8 +- pkg/client/rs/resource_server.go | 6 +- pkg/oidc/code_challenge.go | 2 +- pkg/oidc/token.go | 4 +- pkg/oidc/token_request.go | 10 +- pkg/oidc/verifier.go | 2 +- pkg/op/auth_request.go | 19 +- pkg/op/auth_request_test.go | 8 +- pkg/op/client.go | 2 +- pkg/op/config.go | 86 +++- pkg/op/config_test.go | 300 +++++++++++-- pkg/op/context.go | 49 +++ pkg/op/context_test.go | 76 ++++ pkg/op/crypto.go | 2 +- pkg/op/discovery.go | 242 +++++----- pkg/op/discovery_test.go | 510 ++++++++++++++++++---- pkg/op/endpoint_test.go | 2 +- pkg/op/error.go | 4 +- pkg/op/keys.go | 21 +- pkg/op/keys_test.go | 38 +- pkg/op/mock/authorizer.mock.go | 43 +- pkg/op/mock/authorizer.mock.impl.go | 33 +- pkg/op/mock/client.go | 4 +- pkg/op/mock/client.mock.go | 6 +- pkg/op/mock/configuration.mock.go | 31 +- pkg/op/mock/discovery.mock.go | 51 +++ pkg/op/mock/generate.go | 13 +- pkg/op/mock/key.mock.go | 18 +- pkg/op/mock/signer.mock.go | 138 ++++-- pkg/op/mock/storage.mock.go | 78 ++-- pkg/op/mock/storage.mock.impl.go | 29 +- pkg/op/op.go | 312 +++++++------ pkg/op/probes.go | 11 +- pkg/op/session.go | 8 +- pkg/op/signer.go | 94 +--- pkg/op/storage.go | 22 +- pkg/op/token.go | 54 ++- pkg/op/token_client_credentials.go | 20 +- pkg/op/token_code.go | 4 +- pkg/op/token_intospection.go | 11 +- pkg/op/token_jwt_profile.go | 66 ++- pkg/op/token_refresh.go | 6 +- pkg/op/token_request.go | 12 +- pkg/op/token_revocation.go | 12 +- pkg/op/userinfo.go | 8 +- pkg/op/verifier_access_token.go | 2 +- pkg/op/verifier_id_token_hint.go | 4 +- pkg/op/verifier_jwt_profile.go | 2 +- 77 files changed, 2349 insertions(+), 913 deletions(-) create mode 100644 example/server/dynamic/login.go create mode 100644 example/server/dynamic/op.go create mode 100644 example/server/storage/storage_dynamic.go create mode 100644 pkg/op/context.go create mode 100644 pkg/op/context_test.go create mode 100644 pkg/op/mock/discovery.mock.go diff --git a/.releaserc.js b/.releaserc.js index 6500ace..7b9f1ce 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -1,5 +1,8 @@ module.exports = { - branches: ["main"], + branches: [ + {name: "main"}, + {name: "dynamic-issuer", prerelease: true}, + ], plugins: [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", diff --git a/README.md b/README.md index d0356ce..26a08ec 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ The most important packages of the library: /client/app web app / RP demonstrating authorization code flow using various authentication methods (code, PKCE, JWT profile) /client/github example of the extended OAuth2 library, providing an HTTP client with a reuse token source /client/service demonstration of JWT Profile Authorization Grant - /server example of an OpenID Provider implementation including some very basic login UI + /server examples of an OpenID Provider implementations (including dynamic) with some very basic login UI ## How To Use It @@ -44,16 +44,27 @@ Check the `/example` folder where example code for different scenarios is locate ```bash # start oidc op server # oidc discovery http://localhost:9998/.well-known/openid-configuration -go run github.com/zitadel/oidc/example/server +go run github.com/zitadel/oidc/v2/example/server # start oidc web client (in a new terminal) CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998 SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/example/client/app ``` - open http://localhost:9999/login in your browser - you will be redirected to op server and the login UI -- login with user `test-user` and password `verysecure` +- login with user `test-user@localhost` and password `verysecure` - the OP will redirect you to the client app, which displays the user info +for the dynamic issuer, just start it with: +```bash +go run github.com/zitadel/oidc/v2/example/server/dynamic +``` +the oidc web client above will still work, but if you add `oidc.local` (pointing to 127.0.0.1) in your hosts file you can also start it with: +```bash +CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v2/example/client/app +``` + +> Note: Usernames are suffixed with the hostname (`test-user@localhost` or `test-user@oidc.local`) + ## Features | | Code Flow | Implicit Flow | Hybrid Flow | Discovery | PKCE | Token Exchange | mTLS | JWT Profile | Refresh Token | Client Credentials | diff --git a/example/client/api/api.go b/example/client/api/api.go index 0ab669d..c475354 100644 --- a/example/client/api/api.go +++ b/example/client/api/api.go @@ -12,8 +12,8 @@ import ( "github.com/gorilla/mux" "github.com/sirupsen/logrus" - "github.com/zitadel/oidc/pkg/client/rs" - "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/client/rs" + "github.com/zitadel/oidc/v2/pkg/oidc" ) const ( diff --git a/example/client/app/app.go b/example/client/app/app.go index b7f2868..3e5f19c 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -11,9 +11,9 @@ import ( "github.com/google/uuid" "github.com/sirupsen/logrus" - "github.com/zitadel/oidc/pkg/client/rp" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/client/rp" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) var ( diff --git a/example/client/github/github.go b/example/client/github/github.go index feb3e26..57bb3ae 100644 --- a/example/client/github/github.go +++ b/example/client/github/github.go @@ -10,9 +10,9 @@ import ( "golang.org/x/oauth2" githubOAuth "golang.org/x/oauth2/github" - "github.com/zitadel/oidc/pkg/client/rp" - "github.com/zitadel/oidc/pkg/client/rp/cli" - "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/v2/pkg/client/rp" + "github.com/zitadel/oidc/v2/pkg/client/rp/cli" + "github.com/zitadel/oidc/v2/pkg/http" ) var ( diff --git a/example/client/service/service.go b/example/client/service/service.go index b3819d5..9526174 100644 --- a/example/client/service/service.go +++ b/example/client/service/service.go @@ -13,7 +13,7 @@ import ( "github.com/sirupsen/logrus" "golang.org/x/oauth2" - "github.com/zitadel/oidc/pkg/client/profile" + "github.com/zitadel/oidc/v2/pkg/client/profile" ) var client = http.DefaultClient diff --git a/example/doc.go b/example/doc.go index 7212a7d..fd4f038 100644 --- a/example/doc.go +++ b/example/doc.go @@ -5,7 +5,6 @@ Package example contains some example of the various use of this library: /app web app / RP demonstrating authorization code flow using various authentication methods (code, PKCE, JWT profile) /github example of the extended OAuth2 library, providing an HTTP client with a reuse token source /service demonstration of JWT Profile Authorization Grant -/server example of an OpenID Provider implementation including some very basic login UI - +/server examples of an OpenID Provider implementations (including dynamic) with some very basic */ package example diff --git a/example/server/dynamic/login.go b/example/server/dynamic/login.go new file mode 100644 index 0000000..e7c6e5f --- /dev/null +++ b/example/server/dynamic/login.go @@ -0,0 +1,113 @@ +package main + +import ( + "context" + "fmt" + "html/template" + "net/http" + + "github.com/gorilla/mux" + + "github.com/zitadel/oidc/v2/pkg/op" +) + +const ( + queryAuthRequestID = "authRequestID" +) + +var ( + loginTmpl, _ = template.New("login").Parse(` + + + + + Login + + +
+ +
+ + +
+
+ + +
+

{{.Error}}

+ +
+ + `) +) + +type login struct { + authenticate authenticate + router *mux.Router + callback func(context.Context, string) string +} + +func NewLogin(authenticate authenticate, callback func(context.Context, string) string, issuerInterceptor *op.IssuerInterceptor) *login { + l := &login{ + authenticate: authenticate, + callback: callback, + } + l.createRouter(issuerInterceptor) + return l +} + +func (l *login) createRouter(issuerInterceptor *op.IssuerInterceptor) { + l.router = mux.NewRouter() + l.router.Path("/username").Methods("GET").HandlerFunc(l.loginHandler) + l.router.Path("/username").Methods("POST").HandlerFunc(issuerInterceptor.HandlerFunc(l.checkLoginHandler)) +} + +type authenticate interface { + CheckUsernamePassword(ctx context.Context, username, password, id string) error +} + +func (l *login) loginHandler(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + http.Error(w, fmt.Sprintf("cannot parse form:%s", err), http.StatusInternalServerError) + return + } + //the oidc package will pass the id of the auth request as query parameter + //we will use this id through the login process and therefore pass it to the login page + renderLogin(w, r.FormValue(queryAuthRequestID), nil) +} + +func renderLogin(w http.ResponseWriter, id string, err error) { + var errMsg string + if err != nil { + errMsg = err.Error() + } + data := &struct { + ID string + Error string + }{ + ID: id, + Error: errMsg, + } + err = loginTmpl.Execute(w, data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (l *login) checkLoginHandler(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + http.Error(w, fmt.Sprintf("cannot parse form:%s", err), http.StatusInternalServerError) + return + } + username := r.FormValue("username") + password := r.FormValue("password") + id := r.FormValue("id") + err = l.authenticate.CheckUsernamePassword(r.Context(), username, password, id) + if err != nil { + renderLogin(w, id, err) + return + } + http.Redirect(w, r, l.callback(r.Context(), id), http.StatusFound) +} diff --git a/example/server/dynamic/op.go b/example/server/dynamic/op.go new file mode 100644 index 0000000..02c12b2 --- /dev/null +++ b/example/server/dynamic/op.go @@ -0,0 +1,138 @@ +package main + +import ( + "context" + "crypto/sha256" + "fmt" + "log" + "net/http" + + "github.com/gorilla/mux" + "golang.org/x/text/language" + + "github.com/zitadel/oidc/v2/example/server/storage" + "github.com/zitadel/oidc/v2/pkg/op" +) + +const ( + pathLoggedOut = "/logged-out" +) + +var ( + hostnames = []string{ + "localhost", //note that calling 127.0.0.1 / ::1 won't work as the hostname does not match + "oidc.local", //add this to your hosts file (pointing to 127.0.0.1) + //feel free to add more... + } +) + +func init() { + storage.RegisterClients( + storage.NativeClient("native"), + storage.WebClient("web", "secret"), + storage.WebClient("api", "secret"), + ) +} + +func main() { + ctx := context.Background() + + port := "9998" + issuers := make([]string, len(hostnames)) + for i, hostname := range hostnames { + issuers[i] = fmt.Sprintf("http://%s:%s/", hostname, port) + } + + //the OpenID Provider requires a 32-byte key for (token) encryption + //be sure to create a proper crypto random key and manage it securely! + key := sha256.Sum256([]byte("test")) + + router := mux.NewRouter() + + //for simplicity, we provide a very small default page for users who have signed out + router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) { + _, err := w.Write([]byte("signed out successfully")) + if err != nil { + log.Printf("error serving logged out page: %v", err) + } + }) + + //the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations + //this might be the layer for accessing your database + //in this example it will be handled in-memory + //the NewMultiStorage is able to handle multiple issuers + storage := storage.NewMultiStorage(issuers) + + //creation of the OpenIDProvider with the just created in-memory Storage + provider, err := newDynamicOP(ctx, storage, key) + if err != nil { + log.Fatal(err) + } + + //the provider will only take care of the OpenID Protocol, so there must be some sort of UI for the login process + //for the simplicity of the example this means a simple page with username and password field + //be sure to provide an IssuerInterceptor with the IssuerFromRequest from the OP so the login can select / and pass it to the storage + l := NewLogin(storage, op.AuthCallbackURL(provider), op.NewIssuerInterceptor(provider.IssuerFromRequest)) + + //regardless of how many pages / steps there are in the process, the UI must be registered in the router, + //so we will direct all calls to /login to the login UI + router.PathPrefix("/login/").Handler(http.StripPrefix("/login", l.router)) + + //we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration) + //is served on the correct path + // + //if your issuer ends with a path (e.g. http://localhost:9998/custom/path/), + //then you would have to set the path prefix (/custom/path/): + //router.PathPrefix("/custom/path/").Handler(http.StripPrefix("/custom/path", provider.HttpHandler())) + router.PathPrefix("/").Handler(provider.HttpHandler()) + + server := &http.Server{ + Addr: ":" + port, + Handler: router, + } + err = server.ListenAndServe() + if err != nil { + log.Fatal(err) + } + <-ctx.Done() +} + +// newDynamicOP will create an OpenID Provider for localhost on a specified port with a given encryption key +// and a predefined default logout uri +// it will enable all options (see descriptions) +func newDynamicOP(ctx context.Context, storage op.Storage, key [32]byte) (*op.Provider, error) { + config := &op.Config{ + CryptoKey: key, + + //will be used if the end_session endpoint is called without a post_logout_redirect_uri + DefaultLogoutRedirectURI: pathLoggedOut, + + //enables code_challenge_method S256 for PKCE (and therefore PKCE in general) + CodeMethodS256: true, + + //enables additional client_id/client_secret authentication by form post (not only HTTP Basic Auth) + AuthMethodPost: true, + + //enables additional authentication by using private_key_jwt + AuthMethodPrivateKeyJWT: true, + + //enables refresh_token grant use + GrantTypeRefreshToken: true, + + //enables use of the `request` Object parameter + RequestObjectSupported: true, + + //this example has only static texts (in English), so we'll set the here accordingly + SupportedUILocales: []language.Tag{language.English}, + } + handler, err := op.NewDynamicOpenIDProvider(ctx, "/", config, storage, + //we must explicitly allow the use of the http issuer + op.WithAllowInsecure(), + //as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth + op.WithCustomAuthEndpoint(op.NewEndpoint("auth")), + ) + if err != nil { + return nil, err + } + return handler, nil +} diff --git a/example/server/exampleop/login.go b/example/server/exampleop/login.go index fd3dead..5da86d1 100644 --- a/example/server/exampleop/login.go +++ b/example/server/exampleop/login.go @@ -1,6 +1,7 @@ package exampleop import ( + "context" "fmt" "html/template" "net/http" @@ -44,10 +45,10 @@ var loginTmpl, _ = template.New("login").Parse(` type login struct { authenticate authenticate router *mux.Router - callback func(string) string + callback func(context.Context, string) string } -func NewLogin(authenticate authenticate, callback func(string) string) *login { +func NewLogin(authenticate authenticate, callback func(context.Context, string) string) *login { l := &login{ authenticate: authenticate, callback: callback, @@ -109,5 +110,5 @@ func (l *login) checkLoginHandler(w http.ResponseWriter, r *http.Request) { renderLogin(w, id, err) return } - http.Redirect(w, r, l.callback(id), http.StatusFound) + http.Redirect(w, r, l.callback(r.Context(), id), http.StatusFound) } diff --git a/example/server/exampleop/op.go b/example/server/exampleop/op.go index 4794d8a..d3a450c 100644 --- a/example/server/exampleop/op.go +++ b/example/server/exampleop/op.go @@ -5,13 +5,12 @@ import ( "crypto/sha256" "log" "net/http" - "os" "github.com/gorilla/mux" "golang.org/x/text/language" - "github.com/zitadel/oidc/example/server/storage" - "github.com/zitadel/oidc/pkg/op" + "github.com/zitadel/oidc/v2/example/server/storage" + "github.com/zitadel/oidc/v2/pkg/op" ) const ( @@ -35,9 +34,6 @@ type Storage interface { // // Use one of the pre-made clients in storage/clients.go or register a new one. func SetupServer(ctx context.Context, issuer string, storage Storage) *mux.Router { - // this will allow us to use an issuer with http:// instead of https:// - os.Setenv(op.OidcDevMode, "true") - // the OpenID Provider requires a 32-byte key for (token) encryption // be sure to create a proper crypto random key and manage it securely! key := sha256.Sum256([]byte("test")) @@ -81,7 +77,6 @@ func SetupServer(ctx context.Context, issuer string, storage Storage) *mux.Route // it will enable all options (see descriptions) func newOP(ctx context.Context, storage op.Storage, issuer string, key [32]byte) (op.OpenIDProvider, error) { config := &op.Config{ - Issuer: issuer, CryptoKey: key, // will be used if the end_session endpoint is called without a post_logout_redirect_uri @@ -105,7 +100,9 @@ func newOP(ctx context.Context, storage op.Storage, issuer string, key [32]byte) // this example has only static texts (in English), so we'll set the here accordingly SupportedUILocales: []language.Tag{language.English}, } - handler, err := op.NewOpenIDProvider(ctx, config, storage, + handler, err := op.NewOpenIDProvider(ctx, issuer, config, storage, + //we must explicitly allow the use of the http issuer + op.WithAllowInsecure(), // as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth op.WithCustomAuthEndpoint(op.NewEndpoint("auth")), ) diff --git a/example/server/main.go b/example/server/main.go index 3cfd20d..327e294 100644 --- a/example/server/main.go +++ b/example/server/main.go @@ -2,23 +2,28 @@ package main import ( "context" + "fmt" "log" "net/http" - "github.com/zitadel/oidc/example/server/exampleop" - "github.com/zitadel/oidc/example/server/storage" + "github.com/zitadel/oidc/v2/example/server/exampleop" + "github.com/zitadel/oidc/v2/example/server/storage" ) func main() { ctx := context.Background() + //we will run on :9998 + port := "9998" + //which gives us the issuer: //http://localhost:9998/ + issuer := fmt.Sprintf("http://localhost:%s/", port) + // the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations // this might be the layer for accessing your database // in this example it will be handled in-memory - storage := storage.NewStorage(storage.NewUserStore()) + storage := storage.NewStorage(storage.NewUserStore(issuer)) - port := "9998" - router := exampleop.SetupServer(ctx, "http://localhost:"+port, storage) + router := exampleop.SetupServer(ctx, issuer, storage) server := &http.Server{ Addr: ":" + port, diff --git a/example/server/storage/client.go b/example/server/storage/client.go index 0f3a703..bd6ff3c 100644 --- a/example/server/storage/client.go +++ b/example/server/storage/client.go @@ -3,8 +3,8 @@ package storage import ( "time" - "github.com/zitadel/oidc/pkg/oidc" - "github.com/zitadel/oidc/pkg/op" + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" ) var ( @@ -113,14 +113,14 @@ func (c *Client) IsScopeAllowed(scope string) bool { // IDTokenUserinfoClaimsAssertion allows specifying if claims of scope profile, email, phone and address are asserted into the id_token // even if an access token if issued which violates the OIDC Core spec -//(5.4. Requesting Claims using Scope Values: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) +// (5.4. Requesting Claims using Scope Values: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) // some clients though require that e.g. email is always in the id_token when requested even if an access_token is issued func (c *Client) IDTokenUserinfoClaimsAssertion() bool { return c.idTokenUserinfoClaimsAssertion } // ClockSkew enables clients to instruct the OP to apply a clock skew on the various times and expirations -//(subtract from issued_at, add to expiration, ...) +// (subtract from issued_at, add to expiration, ...) func (c *Client) ClockSkew() time.Duration { return c.clockSkew } @@ -141,7 +141,7 @@ func RegisterClients(registerClients ...*Client) { // user-defined redirectURIs may include: // - http://localhost without port specification (e.g. http://localhost/auth/callback) // - custom protocol (e.g. custom://auth/callback) -//(the examples will be used as default, if none is provided) +// (the examples will be used as default, if none is provided) func NativeClient(id string, redirectURIs ...string) *Client { if len(redirectURIs) == 0 { redirectURIs = []string{ @@ -168,7 +168,7 @@ func NativeClient(id string, redirectURIs ...string) *Client { // WebClient will create a client of type web, which will always use Basic Auth and allow the use of refresh tokens // user-defined redirectURIs may include: // - http://localhost with port specification (e.g. http://localhost:9999/auth/callback) -//(the example will be used as default, if none is provided) +// (the example will be used as default, if none is provided) func WebClient(id, secret string, redirectURIs ...string) *Client { if len(redirectURIs) == 0 { redirectURIs = []string{ diff --git a/example/server/storage/oidc.go b/example/server/storage/oidc.go index 91afd90..505ab72 100644 --- a/example/server/storage/oidc.go +++ b/example/server/storage/oidc.go @@ -5,9 +5,8 @@ import ( "golang.org/x/text/language" - "github.com/zitadel/oidc/pkg/op" - - "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" ) const ( diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index 130822e..64bffc8 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -12,8 +12,8 @@ import ( "github.com/google/uuid" "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/pkg/oidc" - "github.com/zitadel/oidc/pkg/op" + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" ) // serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant @@ -45,9 +45,41 @@ type Storage struct { } type signingKey struct { - ID string - Algorithm string - Key *rsa.PrivateKey + id string + algorithm jose.SignatureAlgorithm + key *rsa.PrivateKey +} + +func (s *signingKey) SignatureAlgorithm() jose.SignatureAlgorithm { + return s.algorithm +} + +func (s *signingKey) Key() interface{} { + return s.key +} + +func (s *signingKey) ID() string { + return s.id +} + +type publicKey struct { + signingKey +} + +func (s *publicKey) ID() string { + return s.id +} + +func (s *publicKey) Algorithm() jose.SignatureAlgorithm { + return s.algorithm +} + +func (s *publicKey) Use() string { + return "sig" +} + +func (s *publicKey) Key() interface{} { + return &s.key.PublicKey } func NewStorage(userStore UserStore) *Storage { @@ -67,9 +99,9 @@ func NewStorage(userStore UserStore) *Storage { }, }, signingKey: signingKey{ - ID: "id", - Algorithm: "RS256", - Key: key, + id: uuid.NewString(), + algorithm: jose.RS256, + key: key, }, } } @@ -288,41 +320,29 @@ func (s *Storage) RevokeToken(ctx context.Context, tokenIDOrToken string, userID return nil } -// GetSigningKey implements the op.Storage interface +// SigningKey implements the op.Storage interface // it will be called when creating the OpenID Provider -func (s *Storage) GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey) { +func (s *Storage) SigningKey(ctx context.Context) (op.SigningKey, error) { // in this example the signing key is a static rsa.PrivateKey and the algorithm used is RS256 // you would obviously have a more complex implementation and store / retrieve the key from your database as well - // - // the idea of the signing key channel is, that you can (with what ever mechanism) rotate your signing key and - // switch the key of the signer via this channel - keyCh <- jose.SigningKey{ - Algorithm: jose.SignatureAlgorithm(s.signingKey.Algorithm), // always tell the signer with algorithm to use - Key: jose.JSONWebKey{ - KeyID: s.signingKey.ID, // always give the key an id so, that it will include it in the token header as `kid` claim - Key: s.signingKey.Key, - }, - } + return &s.signingKey, nil } -// GetKeySet implements the op.Storage interface +// SignatureAlgorithms implements the op.Storage interface +// it will be called to get the sign +func (s *Storage) SignatureAlgorithms(context.Context) ([]jose.SignatureAlgorithm, error) { + return []jose.SignatureAlgorithm{s.signingKey.algorithm}, nil +} + +// KeySet implements the op.Storage interface // it will be called to get the current (public) keys, among others for the keys_endpoint or for validating access_tokens on the userinfo_endpoint, ... -func (s *Storage) GetKeySet(ctx context.Context) (*jose.JSONWebKeySet, error) { +func (s *Storage) KeySet(ctx context.Context) ([]op.Key, error) { // as mentioned above, this example only has a single signing key without key rotation, // so it will directly use its public key // // when using key rotation you typically would store the public keys alongside the private keys in your database - // and give both of them an expiration date, with the public key having a longer lifetime (e.g. rotate private key every - return &jose.JSONWebKeySet{ - Keys: []jose.JSONWebKey{ - { - KeyID: s.signingKey.ID, - Algorithm: s.signingKey.Algorithm, - Use: oidc.KeyUseSignature, - Key: &s.signingKey.Key.PublicKey, - }, - }, - }, nil + //and give both of them an expiration date, with the public key having a longer lifetime + return []op.Key{&publicKey{s.signingKey}}, nil } // GetClientByClientID implements the op.Storage interface diff --git a/example/server/storage/storage_dynamic.go b/example/server/storage/storage_dynamic.go new file mode 100644 index 0000000..ec6a92e --- /dev/null +++ b/example/server/storage/storage_dynamic.go @@ -0,0 +1,260 @@ +package storage + +import ( + "context" + "time" + + "gopkg.in/square/go-jose.v2" + + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" +) + +type multiStorage struct { + issuers map[string]*Storage +} + +// NewMultiStorage implements the op.Storage interface by wrapping multiple storage structs +// and selecting them by the calling issuer +func NewMultiStorage(issuers []string) *multiStorage { + s := make(map[string]*Storage) + for _, issuer := range issuers { + s[issuer] = NewStorage(NewUserStore(issuer)) + } + return &multiStorage{issuers: s} +} + +// CheckUsernamePassword implements the `authenticate` interface of the login +func (s *multiStorage) CheckUsernamePassword(ctx context.Context, username, password, id string) error { + storage, err := s.storageFromContext(ctx) + if err != nil { + return err + } + return storage.CheckUsernamePassword(username, password, id) +} + +// CreateAuthRequest implements the op.Storage interface +// it will be called after parsing and validation of the authentication request +func (s *multiStorage) CreateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, userID string) (op.AuthRequest, error) { + storage, err := s.storageFromContext(ctx) + if err != nil { + return nil, err + } + return storage.CreateAuthRequest(ctx, authReq, userID) +} + +// AuthRequestByID implements the op.Storage interface +// it will be called after the Login UI redirects back to the OIDC endpoint +func (s *multiStorage) AuthRequestByID(ctx context.Context, id string) (op.AuthRequest, error) { + storage, err := s.storageFromContext(ctx) + if err != nil { + return nil, err + } + return storage.AuthRequestByID(ctx, id) +} + +// AuthRequestByCode implements the op.Storage interface +// it will be called after parsing and validation of the token request (in an authorization code flow) +func (s *multiStorage) AuthRequestByCode(ctx context.Context, code string) (op.AuthRequest, error) { + storage, err := s.storageFromContext(ctx) + if err != nil { + return nil, err + } + return storage.AuthRequestByCode(ctx, code) +} + +// SaveAuthCode implements the op.Storage interface +// it will be called after the authentication has been successful and before redirecting the user agent to the redirect_uri +// (in an authorization code flow) +func (s *multiStorage) SaveAuthCode(ctx context.Context, id string, code string) error { + storage, err := s.storageFromContext(ctx) + if err != nil { + return err + } + return storage.SaveAuthCode(ctx, id, code) +} + +// DeleteAuthRequest implements the op.Storage interface +// it will be called after creating the token response (id and access tokens) for a valid +// - authentication request (in an implicit flow) +// - token request (in an authorization code flow) +func (s *multiStorage) DeleteAuthRequest(ctx context.Context, id string) error { + storage, err := s.storageFromContext(ctx) + if err != nil { + return err + } + return storage.DeleteAuthRequest(ctx, id) +} + +// CreateAccessToken implements the op.Storage interface +// it will be called for all requests able to return an access token (Authorization Code Flow, Implicit Flow, JWT Profile, ...) +func (s *multiStorage) CreateAccessToken(ctx context.Context, request op.TokenRequest) (string, time.Time, error) { + storage, err := s.storageFromContext(ctx) + if err != nil { + return "", time.Time{}, err + } + return storage.CreateAccessToken(ctx, request) +} + +// CreateAccessAndRefreshTokens implements the op.Storage interface +// it will be called for all requests able to return an access and refresh token (Authorization Code Flow, Refresh Token Request) +func (s *multiStorage) CreateAccessAndRefreshTokens(ctx context.Context, request op.TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) { + storage, err := s.storageFromContext(ctx) + if err != nil { + return "", "", time.Time{}, err + } + return storage.CreateAccessAndRefreshTokens(ctx, request, currentRefreshToken) +} + +// TokenRequestByRefreshToken implements the op.Storage interface +// it will be called after parsing and validation of the refresh token request +func (s *multiStorage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (op.RefreshTokenRequest, error) { + storage, err := s.storageFromContext(ctx) + if err != nil { + return nil, err + } + return storage.TokenRequestByRefreshToken(ctx, refreshToken) +} + +// TerminateSession implements the op.Storage interface +// it will be called after the user signed out, therefore the access and refresh token of the user of this client must be removed +func (s *multiStorage) TerminateSession(ctx context.Context, userID string, clientID string) error { + storage, err := s.storageFromContext(ctx) + if err != nil { + return err + } + return storage.TerminateSession(ctx, userID, clientID) +} + +// RevokeToken implements the op.Storage interface +// it will be called after parsing and validation of the token revocation request +func (s *multiStorage) RevokeToken(ctx context.Context, token string, userID string, clientID string) *oidc.Error { + storage, err := s.storageFromContext(ctx) + if err != nil { + return err + } + return storage.RevokeToken(ctx, token, userID, clientID) +} + +// SigningKey implements the op.Storage interface +// it will be called when creating the OpenID Provider +func (s *multiStorage) SigningKey(ctx context.Context) (op.SigningKey, error) { + storage, err := s.storageFromContext(ctx) + if err != nil { + return nil, err + } + return storage.SigningKey(ctx) +} + +// SignatureAlgorithms implements the op.Storage interface +// it will be called to get the sign +func (s *multiStorage) SignatureAlgorithms(ctx context.Context) ([]jose.SignatureAlgorithm, error) { + storage, err := s.storageFromContext(ctx) + if err != nil { + return nil, err + } + return storage.SignatureAlgorithms(ctx) +} + +// KeySet implements the op.Storage interface +// it will be called to get the current (public) keys, among others for the keys_endpoint or for validating access_tokens on the userinfo_endpoint, ... +func (s *multiStorage) KeySet(ctx context.Context) ([]op.Key, error) { + storage, err := s.storageFromContext(ctx) + if err != nil { + return nil, err + } + return storage.KeySet(ctx) +} + +// GetClientByClientID implements the op.Storage interface +// it will be called whenever information (type, redirect_uris, ...) about the client behind the client_id is needed +func (s *multiStorage) GetClientByClientID(ctx context.Context, clientID string) (op.Client, error) { + storage, err := s.storageFromContext(ctx) + if err != nil { + return nil, err + } + return storage.GetClientByClientID(ctx, clientID) +} + +// AuthorizeClientIDSecret implements the op.Storage interface +// it will be called for validating the client_id, client_secret on token or introspection requests +func (s *multiStorage) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error { + storage, err := s.storageFromContext(ctx) + if err != nil { + return err + } + return storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret) +} + +// SetUserinfoFromScopes implements the op.Storage interface +// it will be called for the creation of an id_token, so we'll just pass it to the private function without any further check +func (s *multiStorage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error { + storage, err := s.storageFromContext(ctx) + if err != nil { + return err + } + return storage.SetUserinfoFromScopes(ctx, userinfo, userID, clientID, scopes) +} + +// SetUserinfoFromToken implements the op.Storage interface +// it will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function +func (s *multiStorage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error { + storage, err := s.storageFromContext(ctx) + if err != nil { + return err + } + return storage.SetUserinfoFromToken(ctx, userinfo, tokenID, subject, origin) +} + +// SetIntrospectionFromToken implements the op.Storage interface +// it will be called for the introspection endpoint, so we read the token and pass the information from that to the private function +func (s *multiStorage) SetIntrospectionFromToken(ctx context.Context, introspection oidc.IntrospectionResponse, tokenID, subject, clientID string) error { + storage, err := s.storageFromContext(ctx) + if err != nil { + return err + } + return storage.SetIntrospectionFromToken(ctx, introspection, tokenID, subject, clientID) +} + +// GetPrivateClaimsFromScopes implements the op.Storage interface +// it will be called for the creation of a JWT access token to assert claims for custom scopes +func (s *multiStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) { + storage, err := s.storageFromContext(ctx) + if err != nil { + return nil, err + } + return storage.GetPrivateClaimsFromScopes(ctx, userID, clientID, scopes) +} + +// GetKeyByIDAndUserID implements the op.Storage interface +// it will be called to validate the signatures of a JWT (JWT Profile Grant and Authentication) +func (s *multiStorage) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) { + storage, err := s.storageFromContext(ctx) + if err != nil { + return nil, err + } + return storage.GetKeyByIDAndUserID(ctx, keyID, userID) +} + +// ValidateJWTProfileScopes implements the op.Storage interface +// it will be called to validate the scopes of a JWT Profile Authorization Grant request +func (s *multiStorage) ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error) { + storage, err := s.storageFromContext(ctx) + if err != nil { + return nil, err + } + return storage.ValidateJWTProfileScopes(ctx, userID, scopes) +} + +// Health implements the op.Storage interface +func (s *multiStorage) Health(ctx context.Context) error { + return nil +} + +func (s *multiStorage) storageFromContext(ctx context.Context) (*Storage, *oidc.Error) { + storage, ok := s.issuers[op.IssuerFromContext(ctx)] + if !ok { + return nil, oidc.ErrInvalidRequest().WithDescription("invalid issuer") + } + return storage, nil +} diff --git a/example/server/storage/user.go b/example/server/storage/user.go index 423af59..82c06d0 100644 --- a/example/server/storage/user.go +++ b/example/server/storage/user.go @@ -2,6 +2,7 @@ package storage import ( "crypto/rsa" + "strings" "golang.org/x/text/language" ) @@ -33,12 +34,13 @@ type userStore struct { users map[string]*User } -func NewUserStore() UserStore { +func NewUserStore(issuer string) UserStore { + hostname := strings.Split(strings.Split(issuer, "://")[1], ":")[0] return userStore{ users: map[string]*User{ "id1": { ID: "id1", - Username: "test-user", + Username: "test-user@" + hostname, Password: "verysecure", FirstName: "Test", LastName: "User", diff --git a/go.mod b/go.mod index 18ae410..2691e57 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/zitadel/oidc +module github.com/zitadel/oidc/v2 go 1.16 @@ -15,9 +15,8 @@ require ( github.com/rs/cors v1.8.3 github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.1 - github.com/zitadel/logging v0.3.4 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 - golang.org/x/text v0.5.0 + golang.org/x/text v0.6.0 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/square/go-jose.v2 v2.6.0 ) diff --git a/go.sum b/go.sum index ec7b82e..c73eb9d 100644 --- a/go.sum +++ b/go.sum @@ -131,13 +131,11 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo= github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -149,8 +147,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zitadel/logging v0.3.4 h1:9hZsTjMMTE3X2LUi0xcF9Q9EdLo+FAezeu52ireBbHM= -github.com/zitadel/logging v0.3.4/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0= 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= @@ -252,7 +248,6 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -272,7 +267,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220207234003-57398862261d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= @@ -285,8 +279,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 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= @@ -420,10 +414,7 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/client/client.go b/pkg/client/client.go index 62f1019..eaa1a80 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -14,9 +14,9 @@ import ( "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/pkg/crypto" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/crypto" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) var Encoder = func() httphelper.Encoder { diff --git a/pkg/client/jwt_profile.go b/pkg/client/jwt_profile.go index a711de9..1686de6 100644 --- a/pkg/client/jwt_profile.go +++ b/pkg/client/jwt_profile.go @@ -5,8 +5,8 @@ import ( "golang.org/x/oauth2" - "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) // JWTProfileExchange handles the oauth2 jwt profile exchange diff --git a/pkg/client/profile/jwt_profile.go b/pkg/client/profile/jwt_profile.go index b29fcaa..a934f7d 100644 --- a/pkg/client/profile/jwt_profile.go +++ b/pkg/client/profile/jwt_profile.go @@ -7,8 +7,8 @@ import ( "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/pkg/client" - "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/client" + "github.com/zitadel/oidc/v2/pkg/oidc" ) // jwtProfileTokenSource implement the oauth2.TokenSource diff --git a/pkg/client/rp/cli/cli.go b/pkg/client/rp/cli/cli.go index 6e30e4e..936f319 100644 --- a/pkg/client/rp/cli/cli.go +++ b/pkg/client/rp/cli/cli.go @@ -4,9 +4,9 @@ import ( "context" "net/http" - "github.com/zitadel/oidc/pkg/client/rp" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/client/rp" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) const ( diff --git a/pkg/client/rp/delegation.go b/pkg/client/rp/delegation.go index a2b1f00..b16a39e 100644 --- a/pkg/client/rp/delegation.go +++ b/pkg/client/rp/delegation.go @@ -1,13 +1,13 @@ package rp import ( - "github.com/zitadel/oidc/pkg/oidc/grants/tokenexchange" + "github.com/zitadel/oidc/v2/pkg/oidc/grants/tokenexchange" ) // DelegationTokenRequest is an implementation of TokenExchangeRequest // it exchanges an "urn:ietf:params:oauth:token-type:access_token" with an optional -//"urn:ietf:params:oauth:token-type:access_token" actor token for an -//"urn:ietf:params:oauth:token-type:access_token" delegation token +// "urn:ietf:params:oauth:token-type:access_token" actor token for an +// "urn:ietf:params:oauth:token-type:access_token" delegation token func DelegationTokenRequest(subjectToken string, opts ...tokenexchange.TokenExchangeOption) *tokenexchange.TokenExchangeRequest { return tokenexchange.NewTokenExchangeRequest(subjectToken, tokenexchange.AccessTokenType, opts...) } diff --git a/pkg/client/rp/integration_test.go b/pkg/client/rp/integration_test.go index 6f5f489..e29ddd3 100644 --- a/pkg/client/rp/integration_test.go +++ b/pkg/client/rp/integration_test.go @@ -15,28 +15,27 @@ import ( "testing" "time" - "github.com/zitadel/oidc/example/server/exampleop" - "github.com/zitadel/oidc/example/server/storage" - "github.com/jeremija/gosubmit" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/pkg/client/rp" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/v2/example/server/exampleop" + "github.com/zitadel/oidc/v2/example/server/storage" + "github.com/zitadel/oidc/v2/pkg/client/rp" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) func TestRelyingPartySession(t *testing.T) { t.Log("------- start example OP ------") ctx := context.Background() - exampleStorage := storage.NewStorage(storage.NewUserStore()) + targetURL := "http://local-site" + exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL)) var dh deferredHandler opServer := httptest.NewServer(&dh) defer opServer.Close() t.Logf("auth server at %s", opServer.URL) dh.Handler = exampleop.SetupServer(ctx, opServer.URL, exampleStorage) - targetURL := "http://local-site" localURL, err := url.Parse(targetURL + "/login?requestID=1234") require.NoError(t, err, "local url") @@ -109,7 +108,7 @@ func TestRelyingPartySession(t *testing.T) { t.Log("------- post to login form, get redirect to OP ------") postLoginRedirectURL := fillForm(t, "fill login form", httpClient, form, loginPageURL, - gosubmit.Set("username", "test-user"), + gosubmit.Set("username", "test-user@local-site"), gosubmit.Set("password", "verysecure")) t.Logf("Get redirect from %s", postLoginRedirectURL) diff --git a/pkg/client/rp/jwks.go b/pkg/client/rp/jwks.go index cc49eb7..3438bd6 100644 --- a/pkg/client/rp/jwks.go +++ b/pkg/client/rp/jwks.go @@ -9,8 +9,8 @@ import ( "gopkg.in/square/go-jose.v2" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) func NewRemoteKeySet(client *http.Client, jwksURL string, opts ...func(*remoteKeySet)) oidc.KeySet { diff --git a/pkg/client/rp/mock/verifier.mock.go b/pkg/client/rp/mock/verifier.mock.go index b20db68..9d1daa1 100644 --- a/pkg/client/rp/mock/verifier.mock.go +++ b/pkg/client/rp/mock/verifier.mock.go @@ -10,7 +10,7 @@ import ( "github.com/golang/mock/gomock" - "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/oidc" ) // MockVerifier is a mock of Verifier interface diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 86b65da..d2e3cf7 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -14,9 +14,9 @@ import ( "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/pkg/client" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/client" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) const ( @@ -255,7 +255,7 @@ func WithVerifierOpts(opts ...VerifierOption) Option { // WithClientKey specifies the path to the key.json to be used for the JWT Profile Client Authentication on the token endpoint // -//deprecated: use WithJWTProfile(SignerFromKeyPath(path)) instead +// deprecated: use WithJWTProfile(SignerFromKeyPath(path)) instead func WithClientKey(path string) Option { return WithJWTProfile(SignerFromKeyPath(path)) } @@ -304,7 +304,7 @@ func SignerFromKeyAndKeyID(key []byte, keyID string) SignerFromKey { // Discover calls the discovery endpoint of the provided issuer and returns the found endpoints // -//deprecated: use client.Discover +// deprecated: use client.Discover func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint req, err := http.NewRequest("GET", wellKnown, nil) @@ -323,7 +323,7 @@ func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { } // AuthURL returns the auth request url -//(wrapping the oauth2 `AuthCodeURL`) +// (wrapping the oauth2 `AuthCodeURL`) func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string { authOpts := make([]oauth2.AuthCodeOption, 0) for _, opt := range opts { diff --git a/pkg/client/rp/tockenexchange.go b/pkg/client/rp/tockenexchange.go index 3950fe1..c1ac88d 100644 --- a/pkg/client/rp/tockenexchange.go +++ b/pkg/client/rp/tockenexchange.go @@ -5,7 +5,7 @@ import ( "golang.org/x/oauth2" - "github.com/zitadel/oidc/pkg/oidc/grants/tokenexchange" + "github.com/zitadel/oidc/v2/pkg/oidc/grants/tokenexchange" ) // TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange` diff --git a/pkg/client/rp/verifier.go b/pkg/client/rp/verifier.go index 6b3b3fd..f3db128 100644 --- a/pkg/client/rp/verifier.go +++ b/pkg/client/rp/verifier.go @@ -6,7 +6,7 @@ import ( "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/oidc" ) type IDTokenVerifier interface { @@ -20,7 +20,7 @@ type IDTokenVerifier interface { } // VerifyTokens implement the Token Response Validation as defined in OIDC specification -//https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation +// https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation func VerifyTokens(ctx context.Context, accessToken, idTokenString string, v IDTokenVerifier) (oidc.IDTokenClaims, error) { idToken, err := VerifyIDToken(ctx, idTokenString, v) if err != nil { @@ -33,7 +33,7 @@ func VerifyTokens(ctx context.Context, accessToken, idTokenString string, v IDTo } // VerifyIDToken validates the id token according to -//https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation +// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation func VerifyIDToken(ctx context.Context, token string, v IDTokenVerifier) (oidc.IDTokenClaims, error) { claims := oidc.EmptyIDTokenClaims() @@ -89,7 +89,7 @@ func VerifyIDToken(ctx context.Context, token string, v IDTokenVerifier) (oidc.I } // VerifyAccessToken validates the access token according to -//https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation +// https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAlgorithm) error { if atHash == "" { return nil diff --git a/pkg/client/rs/resource_server.go b/pkg/client/rs/resource_server.go index b1bc47e..1d9860f 100644 --- a/pkg/client/rs/resource_server.go +++ b/pkg/client/rs/resource_server.go @@ -6,9 +6,9 @@ import ( "net/http" "time" - "github.com/zitadel/oidc/pkg/client" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/client" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) type ResourceServer interface { diff --git a/pkg/oidc/code_challenge.go b/pkg/oidc/code_challenge.go index e1e459c..37c1783 100644 --- a/pkg/oidc/code_challenge.go +++ b/pkg/oidc/code_challenge.go @@ -3,7 +3,7 @@ package oidc import ( "crypto/sha256" - "github.com/zitadel/oidc/pkg/crypto" + "github.com/zitadel/oidc/v2/pkg/crypto" ) const ( diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index 784684d..198049d 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -9,8 +9,8 @@ import ( "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/pkg/crypto" - "github.com/zitadel/oidc/pkg/http" + "github.com/zitadel/oidc/v2/pkg/crypto" + "github.com/zitadel/oidc/v2/pkg/http" ) const ( diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index ec11057..2b56535 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -214,8 +214,10 @@ type TokenExchangeRequest struct { } type ClientCredentialsRequest struct { - GrantType GrantType `schema:"grant_type"` - Scope SpaceDelimitedArray `schema:"scope"` - ClientID string `schema:"client_id"` - ClientSecret string `schema:"client_secret"` + GrantType GrantType `schema:"grant_type"` + Scope SpaceDelimitedArray `schema:"scope"` + ClientID string `schema:"client_id"` + ClientSecret string `schema:"client_secret"` + ClientAssertion string `schema:"client_assertion"` + ClientAssertionType string `schema:"client_assertion_type"` } diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go index cc18c80..1757651 100644 --- a/pkg/oidc/verifier.go +++ b/pkg/oidc/verifier.go @@ -12,7 +12,7 @@ import ( "gopkg.in/square/go-jose.v2" - str "github.com/zitadel/oidc/pkg/strings" + str "github.com/zitadel/oidc/v2/pkg/strings" ) type Claims interface { diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index d8c960e..b13f642 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -11,9 +11,9 @@ import ( "github.com/gorilla/mux" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" - str "github.com/zitadel/oidc/pkg/strings" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" + str "github.com/zitadel/oidc/v2/pkg/strings" ) type AuthRequest interface { @@ -38,10 +38,8 @@ type Authorizer interface { Storage() Storage Decoder() httphelper.Decoder Encoder() httphelper.Encoder - Signer() Signer - IDTokenHintVerifier() IDTokenHintVerifier + IDTokenHintVerifier(context.Context) IDTokenHintVerifier Crypto() Crypto - Issuer() string RequestObjectSupported() bool } @@ -72,8 +70,9 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { AuthRequestError(w, r, authReq, err, authorizer.Encoder()) return } + ctx := r.Context() if authReq.RequestParam != "" && authorizer.RequestObjectSupported() { - authReq, err = ParseRequestObject(r.Context(), authReq, authorizer.Storage(), authorizer.Issuer()) + authReq, err = ParseRequestObject(ctx, authReq, authorizer.Storage(), IssuerFromContext(ctx)) if err != nil { AuthRequestError(w, r, authReq, err, authorizer.Encoder()) return @@ -91,7 +90,7 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { if validater, ok := authorizer.(AuthorizeValidator); ok { validation = validater.ValidateAuthRequest } - userID, err := validation(r.Context(), authReq, authorizer.Storage(), authorizer.IDTokenHintVerifier()) + userID, err := validation(ctx, authReq, authorizer.Storage(), authorizer.IDTokenHintVerifier(ctx)) if err != nil { AuthRequestError(w, r, authReq, err, authorizer.Encoder()) return @@ -100,12 +99,12 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { AuthRequestError(w, r, authReq, oidc.ErrRequestNotSupported(), authorizer.Encoder()) return } - req, err := authorizer.Storage().CreateAuthRequest(r.Context(), authReq, userID) + req, err := authorizer.Storage().CreateAuthRequest(ctx, authReq, userID) if err != nil { AuthRequestError(w, r, authReq, oidc.DefaultToServerError(err, "unable to save auth request"), authorizer.Encoder()) return } - client, err := authorizer.Storage().GetClientByClientID(r.Context(), req.GetClientID()) + client, err := authorizer.Storage().GetClientByClientID(ctx, req.GetClientID()) if err != nil { AuthRequestError(w, r, req, oidc.DefaultToServerError(err, "unable to retrieve client by id"), authorizer.Encoder()) return diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index dc6f655..7a9701b 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -13,10 +13,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" - "github.com/zitadel/oidc/pkg/op" - "github.com/zitadel/oidc/pkg/op/mock" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v2/pkg/op/mock" ) // diff --git a/pkg/op/client.go b/pkg/op/client.go index d9f7ab0..e8a3347 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -3,7 +3,7 @@ package op import ( "time" - "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/oidc" ) //go:generate go get github.com/dmarkham/enumer diff --git a/pkg/op/config.go b/pkg/op/config.go index 82cbb47..c40fa2d 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -2,20 +2,24 @@ package op import ( "errors" + "net/http" "net/url" - "os" + "strings" "golang.org/x/text/language" ) -const ( - OidcDevMode = "ZITADEL_OIDC_DEV" - // deprecated: use OidcDevMode (ZITADEL_OIDC_DEV=true) - devMode = "CAOS_OIDC_DEV" +var ( + ErrInvalidIssuerPath = errors.New("no fragments or query allowed for issuer") + ErrInvalidIssuerNoIssuer = errors.New("missing issuer") + ErrInvalidIssuerURL = errors.New("invalid url for issuer") + ErrInvalidIssuerMissingHost = errors.New("host for issuer missing") + ErrInvalidIssuerHTTPS = errors.New("scheme for issuer must be `https`") ) type Configuration interface { - Issuer() string + IssuerFromRequest(r *http.Request) string + Insecure() bool AuthorizationEndpoint() Endpoint TokenEndpoint() Endpoint IntrospectionEndpoint() Endpoint @@ -42,36 +46,74 @@ type Configuration interface { SupportedUILocales() []language.Tag } -func ValidateIssuer(issuer string) error { +type IssuerFromRequest func(r *http.Request) string + +func IssuerFromHost(path string) func(bool) (IssuerFromRequest, error) { + return func(allowInsecure bool) (IssuerFromRequest, error) { + issuerPath, err := url.Parse(path) + if err != nil { + return nil, ErrInvalidIssuerURL + } + if err := ValidateIssuerPath(issuerPath); err != nil { + return nil, err + } + return func(r *http.Request) string { + return dynamicIssuer(r.Host, path, allowInsecure) + }, nil + } +} + +func StaticIssuer(issuer string) func(bool) (IssuerFromRequest, error) { + return func(allowInsecure bool) (IssuerFromRequest, error) { + if err := ValidateIssuer(issuer, allowInsecure); err != nil { + return nil, err + } + return func(_ *http.Request) string { + return issuer + }, nil + } +} + +func ValidateIssuer(issuer string, allowInsecure bool) error { if issuer == "" { - return errors.New("missing issuer") + return ErrInvalidIssuerNoIssuer } u, err := url.Parse(issuer) if err != nil { - return errors.New("invalid url for issuer") + return ErrInvalidIssuerURL } if u.Host == "" { - return errors.New("host for issuer missing") + return ErrInvalidIssuerMissingHost } if u.Scheme != "https" { - if !devLocalAllowed(u) { - return errors.New("scheme for issuer must be `https`") + if !devLocalAllowed(u, allowInsecure) { + return ErrInvalidIssuerHTTPS } } - if u.Fragment != "" || len(u.Query()) > 0 { - return errors.New("no fragments or query allowed for issuer") + return ValidateIssuerPath(u) +} + +func ValidateIssuerPath(issuer *url.URL) error { + if issuer.Fragment != "" || len(issuer.Query()) > 0 { + return ErrInvalidIssuerPath } return nil } -func devLocalAllowed(url *url.URL) bool { - _, b := os.LookupEnv(OidcDevMode) - if !b { - // check the old / current env var as well - _, b = os.LookupEnv(devMode) - if !b { - return b - } +func devLocalAllowed(url *url.URL, allowInsecure bool) bool { + if !allowInsecure { + return false } return url.Scheme == "http" } + +func dynamicIssuer(issuer, path string, allowInsecure bool) string { + schema := "https" + if allowInsecure { + schema = "http" + } + if len(path) > 0 && !strings.HasPrefix(path, "/") { + path = "/" + path + } + return schema + "://" + issuer + path +} diff --git a/pkg/op/config_test.go b/pkg/op/config_test.go index 9ff75f1..cfe4e61 100644 --- a/pkg/op/config_test.go +++ b/pkg/op/config_test.go @@ -1,13 +1,17 @@ package op import ( - "os" + "net/http/httptest" + "net/url" "testing" + + "github.com/stretchr/testify/assert" ) func TestValidateIssuer(t *testing.T) { type args struct { - issuer string + issuer string + allowInsecure bool } tests := []struct { name string @@ -16,65 +20,97 @@ func TestValidateIssuer(t *testing.T) { }{ { "missing issuer fails", - args{""}, + args{ + issuer: "", + }, true, }, { "invalid url for issuer fails", - args{":issuer"}, - true, - }, - { - "invalid url for issuer fails", - args{":issuer"}, + args{ + issuer: ":issuer", + }, true, }, { "host for issuer missing fails", - args{"https:///issuer"}, - true, - }, - { - "host for not https fails", - args{"http://issuer.com"}, + args{ + issuer: "https:///issuer", + }, true, }, { "host with fragment fails", - args{"https://issuer.com/#issuer"}, + args{ + issuer: "https://issuer.com/#issuer", + }, true, }, { "host with query fails", - args{"https://issuer.com?issuer=me"}, + args{ + issuer: "https://issuer.com?issuer=me", + }, + true, + }, + { + "host with http fails", + args{ + issuer: "http://issuer.com", + }, true, }, { "host with https ok", - args{"https://issuer.com"}, + args{ + issuer: "https://issuer.com", + }, false, }, { - "localhost with http fails", - args{"http://localhost:9999"}, + "custom scheme fails", + args{ + issuer: "custom://localhost:9999", + }, + true, + }, + { + "http with allowInsecure ok", + args{ + issuer: "http://localhost:9999", + allowInsecure: true, + }, + false, + }, + { + "https with allowInsecure ok", + args{ + issuer: "https://localhost:9999", + allowInsecure: true, + }, + false, + }, + { + "custom scheme with allowInsecure fails", + args{ + issuer: "custom://localhost:9999", + allowInsecure: true, + }, true, }, } - // ensure env is not set - //nolint:errcheck - os.Unsetenv(OidcDevMode) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := ValidateIssuer(tt.args.issuer); (err != nil) != tt.wantErr { + if err := ValidateIssuer(tt.args.issuer, tt.args.allowInsecure); (err != nil) != tt.wantErr { t.Errorf("ValidateIssuer() error = %v, wantErr %v", err, tt.wantErr) } }) } } -func TestValidateIssuerDevLocalAllowed(t *testing.T) { +func TestValidateIssuerPath(t *testing.T) { type args struct { - issuer string + issuerPath *url.URL } tests := []struct { name string @@ -82,17 +118,217 @@ func TestValidateIssuerDevLocalAllowed(t *testing.T) { wantErr bool }{ { - "localhost with http with dev ok", - args{"http://localhost:9999"}, + "empty ok", + args{func() *url.URL { + u, _ := url.Parse("") + return u + }()}, false, }, + { + "custom ok", + args{func() *url.URL { + u, _ := url.Parse("/custom") + return u + }()}, + false, + }, + { + "fragment fails", + args{func() *url.URL { + u, _ := url.Parse("#fragment") + return u + }()}, + true, + }, + { + "query fails", + args{func() *url.URL { + u, _ := url.Parse("?query=value") + return u + }()}, + true, + }, } - //nolint:errcheck - os.Setenv(OidcDevMode, "true") for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := ValidateIssuer(tt.args.issuer); (err != nil) != tt.wantErr { - t.Errorf("ValidateIssuer() error = %v, wantErr %v", err, tt.wantErr) + if err := ValidateIssuerPath(tt.args.issuerPath); (err != nil) != tt.wantErr { + t.Errorf("ValidateIssuerPath() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestIssuerFromHost(t *testing.T) { + type args struct { + path string + allowInsecure bool + target string + } + type res struct { + issuer string + err error + } + tests := []struct { + name string + args args + res res + }{ + { + "invalid issuer path", + args{ + path: "/#fragment", + allowInsecure: false, + }, + res{ + issuer: "", + err: ErrInvalidIssuerPath, + }, + }, + { + "empty path secure", + args{ + path: "", + allowInsecure: false, + target: "https://issuer.com", + }, + res{ + issuer: "https://issuer.com", + err: nil, + }, + }, + { + "custom path secure", + args{ + path: "/custom/", + allowInsecure: false, + target: "https://issuer.com", + }, + res{ + issuer: "https://issuer.com/custom/", + err: nil, + }, + }, + { + "custom path no leading slash", + args{ + path: "custom/", + allowInsecure: false, + target: "https://issuer.com", + }, + res{ + issuer: "https://issuer.com/custom/", + err: nil, + }, + }, + { + "empty path unsecure", + args{ + path: "", + allowInsecure: true, + target: "http://issuer.com", + }, + res{ + issuer: "http://issuer.com", + err: nil, + }, + }, + { + "custom path unsecure", + args{ + path: "/custom/", + allowInsecure: true, + target: "http://issuer.com", + }, + res{ + issuer: "http://issuer.com/custom/", + err: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + issuer, err := IssuerFromHost(tt.args.path)(tt.args.allowInsecure) + if tt.res.err == nil { + assert.NoError(t, err) + req := httptest.NewRequest("", tt.args.target, nil) + assert.Equal(t, tt.res.issuer, issuer(req)) + } + if tt.res.err != nil { + assert.ErrorIs(t, err, tt.res.err) + } + }) + } +} + +func TestStaticIssuer(t *testing.T) { + type args struct { + issuer string + allowInsecure bool + } + type res struct { + issuer string + err error + } + tests := []struct { + name string + args args + res res + }{ + { + "invalid issuer", + args{ + issuer: "", + allowInsecure: false, + }, + res{ + issuer: "", + err: ErrInvalidIssuerNoIssuer, + }, + }, + { + "empty path secure", + args{ + issuer: "https://issuer.com", + allowInsecure: false, + }, + res{ + issuer: "https://issuer.com", + err: nil, + }, + }, + { + "custom path secure", + args{ + issuer: "https://issuer.com/custom/", + allowInsecure: false, + }, + res{ + issuer: "https://issuer.com/custom/", + err: nil, + }, + }, + { + "unsecure", + args{ + issuer: "http://issuer.com", + allowInsecure: true, + }, + res{ + issuer: "http://issuer.com", + err: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + issuer, err := StaticIssuer(tt.args.issuer)(tt.args.allowInsecure) + if tt.res.err == nil { + assert.NoError(t, err) + assert.Equal(t, tt.res.issuer, issuer(nil)) + } + if tt.res.err != nil { + assert.ErrorIs(t, err, tt.res.err) } }) } diff --git a/pkg/op/context.go b/pkg/op/context.go new file mode 100644 index 0000000..4406273 --- /dev/null +++ b/pkg/op/context.go @@ -0,0 +1,49 @@ +package op + +import ( + "context" + "net/http" +) + +type key int + +var ( + issuer key = 0 +) + +type IssuerInterceptor struct { + issuerFromRequest IssuerFromRequest +} + +//NewIssuerInterceptor will set the issuer into the context +//by the provided IssuerFromRequest (e.g. returned from StaticIssuer or IssuerFromHost) +func NewIssuerInterceptor(issuerFromRequest IssuerFromRequest) *IssuerInterceptor { + return &IssuerInterceptor{ + issuerFromRequest: issuerFromRequest, + } +} + +func (i *IssuerInterceptor) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + i.setIssuerCtx(w, r, next) + }) +} + +func (i *IssuerInterceptor) HandlerFunc(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + i.setIssuerCtx(w, r, next) + } +} + +//IssuerFromContext reads the issuer from the context (set by an IssuerInterceptor) +//it will return an empty string if not found +func IssuerFromContext(ctx context.Context) string { + ctxIssuer, _ := ctx.Value(issuer).(string) + return ctxIssuer +} + +func (i *IssuerInterceptor) setIssuerCtx(w http.ResponseWriter, r *http.Request, next http.Handler) { + ctx := context.WithValue(r.Context(), issuer, i.issuerFromRequest(r)) + r = r.WithContext(ctx) + next.ServeHTTP(w, r) +} diff --git a/pkg/op/context_test.go b/pkg/op/context_test.go new file mode 100644 index 0000000..e6bfcec --- /dev/null +++ b/pkg/op/context_test.go @@ -0,0 +1,76 @@ +package op + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIssuerInterceptor(t *testing.T) { + type fields struct { + issuerFromRequest IssuerFromRequest + } + type args struct { + r *http.Request + next http.Handler + } + type res struct { + issuer string + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "empty", + fields{ + func(r *http.Request) string { + return "" + }, + }, + args{}, + res{ + issuer: "", + }, + }, + { + "static", + fields{ + func(r *http.Request) string { + return "static" + }, + }, + args{}, + res{ + issuer: "static", + }, + }, + { + "host", + fields{ + func(r *http.Request) string { + return r.Host + }, + }, + args{}, + res{ + issuer: "issuer.com", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + i := NewIssuerInterceptor(tt.fields.issuerFromRequest) + next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + assert.Equal(t, tt.res.issuer, IssuerFromContext(r.Context())) + }) + req := httptest.NewRequest("", "https://issuer.com", nil) + i.Handler(next).ServeHTTP(nil, req) + i.HandlerFunc(next).ServeHTTP(nil, req) + }) + } +} diff --git a/pkg/op/crypto.go b/pkg/op/crypto.go index f14b1de..6786022 100644 --- a/pkg/op/crypto.go +++ b/pkg/op/crypto.go @@ -1,7 +1,7 @@ package op import ( - "github.com/zitadel/oidc/pkg/crypto" + "github.com/zitadel/oidc/v2/pkg/crypto" ) type Crypto interface { diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 100bfc8..9a25afc 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -1,49 +1,17 @@ package op import ( + "context" "net/http" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" + "gopkg.in/square/go-jose.v2" + + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) -func discoveryHandler(c Configuration, s Signer) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - Discover(w, CreateDiscoveryConfig(c, s)) - } -} - -func Discover(w http.ResponseWriter, config *oidc.DiscoveryConfiguration) { - httphelper.MarshalJSON(w, config) -} - -func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfiguration { - return &oidc.DiscoveryConfiguration{ - Issuer: c.Issuer(), - AuthorizationEndpoint: c.AuthorizationEndpoint().Absolute(c.Issuer()), - TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()), - IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()), - UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()), - RevocationEndpoint: c.RevocationEndpoint().Absolute(c.Issuer()), - EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), - JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), - ScopesSupported: Scopes(c), - ResponseTypesSupported: ResponseTypes(c), - GrantTypesSupported: GrantTypes(c), - SubjectTypesSupported: SubjectTypes(c), - IDTokenSigningAlgValuesSupported: SigAlgorithms(s), - RequestObjectSigningAlgValuesSupported: RequestObjectSigAlgorithms(c), - TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(c), - TokenEndpointAuthSigningAlgValuesSupported: TokenSigAlgorithms(c), - IntrospectionEndpointAuthSigningAlgValuesSupported: IntrospectionSigAlgorithms(c), - IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(c), - RevocationEndpointAuthSigningAlgValuesSupported: RevocationSigAlgorithms(c), - RevocationEndpointAuthMethodsSupported: AuthMethodsRevocationEndpoint(c), - ClaimsSupported: SupportedClaims(c), - CodeChallengeMethodsSupported: CodeChallengeMethods(c), - UILocalesSupported: c.SupportedUILocales(), - RequestParameterSupported: c.RequestObjectSupported(), - } +type DiscoverStorage interface { + SignatureAlgorithms(context.Context) ([]jose.SignatureAlgorithm, error) } var DefaultSupportedScopes = []string{ @@ -55,6 +23,46 @@ var DefaultSupportedScopes = []string{ oidc.ScopeOfflineAccess, } +func discoveryHandler(c Configuration, s DiscoverStorage) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + Discover(w, CreateDiscoveryConfig(r, c, s)) + } +} + +func Discover(w http.ResponseWriter, config *oidc.DiscoveryConfiguration) { + httphelper.MarshalJSON(w, config) +} + +func CreateDiscoveryConfig(r *http.Request, config Configuration, storage DiscoverStorage) *oidc.DiscoveryConfiguration { + issuer := config.IssuerFromRequest(r) + return &oidc.DiscoveryConfiguration{ + Issuer: issuer, + AuthorizationEndpoint: config.AuthorizationEndpoint().Absolute(issuer), + TokenEndpoint: config.TokenEndpoint().Absolute(issuer), + IntrospectionEndpoint: config.IntrospectionEndpoint().Absolute(issuer), + UserinfoEndpoint: config.UserinfoEndpoint().Absolute(issuer), + RevocationEndpoint: config.RevocationEndpoint().Absolute(issuer), + EndSessionEndpoint: config.EndSessionEndpoint().Absolute(issuer), + JwksURI: config.KeysEndpoint().Absolute(issuer), + ScopesSupported: Scopes(config), + ResponseTypesSupported: ResponseTypes(config), + GrantTypesSupported: GrantTypes(config), + SubjectTypesSupported: SubjectTypes(config), + IDTokenSigningAlgValuesSupported: SigAlgorithms(r.Context(), storage), + RequestObjectSigningAlgValuesSupported: RequestObjectSigAlgorithms(config), + TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(config), + TokenEndpointAuthSigningAlgValuesSupported: TokenSigAlgorithms(config), + IntrospectionEndpointAuthSigningAlgValuesSupported: IntrospectionSigAlgorithms(config), + IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(config), + RevocationEndpointAuthSigningAlgValuesSupported: RevocationSigAlgorithms(config), + RevocationEndpointAuthMethodsSupported: AuthMethodsRevocationEndpoint(config), + ClaimsSupported: SupportedClaims(config), + CodeChallengeMethodsSupported: CodeChallengeMethods(config), + UILocalesSupported: config.SupportedUILocales(), + RequestParameterSupported: config.RequestObjectSupported(), + } +} + func Scopes(c Configuration) []string { return DefaultSupportedScopes // TODO: config } @@ -87,6 +95,88 @@ func GrantTypes(c Configuration) []oidc.GrantType { return grantTypes } +func SubjectTypes(c Configuration) []string { + return []string{"public"} //TODO: config +} + +func SigAlgorithms(ctx context.Context, storage DiscoverStorage) []string { + algorithms, err := storage.SignatureAlgorithms(ctx) + if err != nil { + return nil + } + algs := make([]string, len(algorithms)) + for i, algorithm := range algorithms { + algs[i] = string(algorithm) + } + return algs +} + +func RequestObjectSigAlgorithms(c Configuration) []string { + if !c.RequestObjectSupported() { + return nil + } + return c.RequestObjectSigningAlgorithmsSupported() +} + +func AuthMethodsTokenEndpoint(c Configuration) []oidc.AuthMethod { + authMethods := []oidc.AuthMethod{ + oidc.AuthMethodNone, + oidc.AuthMethodBasic, + } + if c.AuthMethodPostSupported() { + authMethods = append(authMethods, oidc.AuthMethodPost) + } + if c.AuthMethodPrivateKeyJWTSupported() { + authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT) + } + return authMethods +} + +func TokenSigAlgorithms(c Configuration) []string { + if !c.AuthMethodPrivateKeyJWTSupported() { + return nil + } + return c.TokenEndpointSigningAlgorithmsSupported() +} + +func IntrospectionSigAlgorithms(c Configuration) []string { + if !c.IntrospectionAuthMethodPrivateKeyJWTSupported() { + return nil + } + return c.IntrospectionEndpointSigningAlgorithmsSupported() +} + +func AuthMethodsIntrospectionEndpoint(c Configuration) []oidc.AuthMethod { + authMethods := []oidc.AuthMethod{ + oidc.AuthMethodBasic, + } + if c.AuthMethodPrivateKeyJWTSupported() { + authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT) + } + return authMethods +} + +func RevocationSigAlgorithms(c Configuration) []string { + if !c.RevocationAuthMethodPrivateKeyJWTSupported() { + return nil + } + return c.RevocationEndpointSigningAlgorithmsSupported() +} + +func AuthMethodsRevocationEndpoint(c Configuration) []oidc.AuthMethod { + authMethods := []oidc.AuthMethod{ + oidc.AuthMethodNone, + oidc.AuthMethodBasic, + } + if c.AuthMethodPostSupported() { + authMethods = append(authMethods, oidc.AuthMethodPost) + } + if c.AuthMethodPrivateKeyJWTSupported() { + authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT) + } + return authMethods +} + func SupportedClaims(c Configuration) []string { return []string{ // TODO: config "sub", @@ -116,59 +206,6 @@ func SupportedClaims(c Configuration) []string { } } -func SigAlgorithms(s Signer) []string { - return []string{string(s.SignatureAlgorithm())} -} - -func SubjectTypes(c Configuration) []string { - return []string{"public"} // TODO: config -} - -func AuthMethodsTokenEndpoint(c Configuration) []oidc.AuthMethod { - authMethods := []oidc.AuthMethod{ - oidc.AuthMethodNone, - oidc.AuthMethodBasic, - } - if c.AuthMethodPostSupported() { - authMethods = append(authMethods, oidc.AuthMethodPost) - } - if c.AuthMethodPrivateKeyJWTSupported() { - authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT) - } - return authMethods -} - -func TokenSigAlgorithms(c Configuration) []string { - if !c.AuthMethodPrivateKeyJWTSupported() { - return nil - } - return c.TokenEndpointSigningAlgorithmsSupported() -} - -func AuthMethodsIntrospectionEndpoint(c Configuration) []oidc.AuthMethod { - authMethods := []oidc.AuthMethod{ - oidc.AuthMethodBasic, - } - if c.AuthMethodPrivateKeyJWTSupported() { - authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT) - } - return authMethods -} - -func AuthMethodsRevocationEndpoint(c Configuration) []oidc.AuthMethod { - authMethods := []oidc.AuthMethod{ - oidc.AuthMethodNone, - oidc.AuthMethodBasic, - } - if c.AuthMethodPostSupported() { - authMethods = append(authMethods, oidc.AuthMethodPost) - } - if c.AuthMethodPrivateKeyJWTSupported() { - authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT) - } - return authMethods -} - func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod { codeMethods := make([]oidc.CodeChallengeMethod, 0, 1) if c.CodeMethodS256Supported() { @@ -176,24 +213,3 @@ func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod { } return codeMethods } - -func IntrospectionSigAlgorithms(c Configuration) []string { - if !c.IntrospectionAuthMethodPrivateKeyJWTSupported() { - return nil - } - return c.IntrospectionEndpointSigningAlgorithmsSupported() -} - -func RevocationSigAlgorithms(c Configuration) []string { - if !c.RevocationAuthMethodPrivateKeyJWTSupported() { - return nil - } - return c.RevocationEndpointSigningAlgorithmsSupported() -} - -func RequestObjectSigAlgorithms(c Configuration) []string { - if !c.RequestObjectSupported() { - return nil - } - return c.RequestObjectSigningAlgorithmsSupported() -} diff --git a/pkg/op/discovery_test.go b/pkg/op/discovery_test.go index 1d74f75..e1b07dd 100644 --- a/pkg/op/discovery_test.go +++ b/pkg/op/discovery_test.go @@ -1,18 +1,19 @@ package op_test import ( + "context" "net/http" "net/http/httptest" - "reflect" "testing" "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/pkg/oidc" - "github.com/zitadel/oidc/pkg/op" - "github.com/zitadel/oidc/pkg/op/mock" + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v2/pkg/op/mock" ) func TestDiscover(t *testing.T) { @@ -47,8 +48,9 @@ func TestDiscover(t *testing.T) { func TestCreateDiscoveryConfig(t *testing.T) { type args struct { - c op.Configuration - s op.Signer + request *http.Request + c op.Configuration + s op.DiscoverStorage } tests := []struct { name string @@ -59,9 +61,8 @@ func TestCreateDiscoveryConfig(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := op.CreateDiscoveryConfig(tt.args.c, tt.args.s); !reflect.DeepEqual(got, tt.want) { - t.Errorf("CreateDiscoveryConfig() = %v, want %v", got, tt.want) - } + got := op.CreateDiscoveryConfig(tt.args.request, tt.args.c, tt.args.s) + assert.Equal(t, tt.want, got) }) } } @@ -83,9 +84,8 @@ func Test_scopes(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := op.Scopes(tt.args.c); !reflect.DeepEqual(got, tt.want) { - t.Errorf("scopes() = %v, want %v", got, tt.want) - } + got := op.Scopes(tt.args.c) + assert.Equal(t, tt.want, got) }) } } @@ -99,13 +99,16 @@ func Test_ResponseTypes(t *testing.T) { args args want []string }{ - // TODO: Add test cases. + { + "code and implicit flow", + args{}, + []string{"code", "id_token", "id_token token"}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := op.ResponseTypes(tt.args.c); !reflect.DeepEqual(got, tt.want) { - t.Errorf("responseTypes() = %v, want %v", got, tt.want) - } + got := op.ResponseTypes(tt.args.c) + assert.Equal(t, tt.want, got) }) } } @@ -117,63 +120,51 @@ func Test_GrantTypes(t *testing.T) { tests := []struct { name string args args - want []string - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := op.GrantTypes(tt.args.c); !reflect.DeepEqual(got, tt.want) { - t.Errorf("grantTypes() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestSupportedClaims(t *testing.T) { - type args struct { - c op.Configuration - } - tests := []struct { - name string - args args - want []string - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := op.SupportedClaims(tt.args.c); !reflect.DeepEqual(got, tt.want) { - t.Errorf("SupportedClaims() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_SigAlgorithms(t *testing.T) { - m := mock.NewMockSigner(gomock.NewController(t)) - type args struct { - s op.Signer - } - tests := []struct { - name string - args args - want []string + want []oidc.GrantType }{ { - "", - args{func() op.Signer { - m.EXPECT().SignatureAlgorithm().Return(jose.RS256) - return m - }()}, - []string{"RS256"}, + "code and implicit flow", + args{ + func() op.Configuration { + c := mock.NewMockConfiguration(gomock.NewController(t)) + c.EXPECT().GrantTypeRefreshTokenSupported().Return(false) + c.EXPECT().GrantTypeTokenExchangeSupported().Return(false) + c.EXPECT().GrantTypeJWTAuthorizationSupported().Return(false) + c.EXPECT().GrantTypeClientCredentialsSupported().Return(false) + return c + }(), + }, + []oidc.GrantType{ + oidc.GrantTypeCode, + oidc.GrantTypeImplicit, + }, + }, + { + "code, implicit flow, refresh token, token exchange, jwt profile, client_credentials", + args{ + func() op.Configuration { + c := mock.NewMockConfiguration(gomock.NewController(t)) + c.EXPECT().GrantTypeRefreshTokenSupported().Return(true) + c.EXPECT().GrantTypeTokenExchangeSupported().Return(true) + c.EXPECT().GrantTypeJWTAuthorizationSupported().Return(true) + c.EXPECT().GrantTypeClientCredentialsSupported().Return(true) + return c + }(), + }, + []oidc.GrantType{ + oidc.GrantTypeCode, + oidc.GrantTypeImplicit, + oidc.GrantTypeRefreshToken, + oidc.GrantTypeClientCredentials, + oidc.GrantTypeTokenExchange, + oidc.GrantTypeBearer, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := op.SigAlgorithms(tt.args.s); !reflect.DeepEqual(got, tt.want) { - t.Errorf("sigAlgorithms() = %v, want %v", got, tt.want) - } + got := op.GrantTypes(tt.args.c) + assert.Equal(t, tt.want, got) }) } } @@ -195,9 +186,80 @@ func Test_SubjectTypes(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := op.SubjectTypes(tt.args.c); !reflect.DeepEqual(got, tt.want) { - t.Errorf("subjectTypes() = %v, want %v", got, tt.want) - } + got := op.SubjectTypes(tt.args.c) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_SigAlgorithms(t *testing.T) { + m := mock.NewMockDiscoverStorage(gomock.NewController(t)) + type args struct { + s op.DiscoverStorage + } + tests := []struct { + name string + args args + want []string + }{ + { + "", + args{func() op.DiscoverStorage { + m.EXPECT().SignatureAlgorithms(gomock.Any()).Return([]jose.SignatureAlgorithm{jose.RS256}, nil) + return m + }()}, + []string{"RS256"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := op.SigAlgorithms(context.Background(), tt.args.s) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_RequestObjectSigAlgorithms(t *testing.T) { + m := mock.NewMockConfiguration(gomock.NewController(t)) + type args struct { + c op.Configuration + } + tests := []struct { + name string + args args + want []string + }{ + { + "not supported, empty", + args{func() op.Configuration { + m.EXPECT().RequestObjectSupported().Return(false) + return m + }()}, + nil, + }, + { + "supported, empty", + args{func() op.Configuration { + m.EXPECT().RequestObjectSupported().Return(true) + m.EXPECT().RequestObjectSigningAlgorithmsSupported().Return(nil) + return m + }()}, + nil, + }, + { + "supported, list", + args{func() op.Configuration { + m.EXPECT().RequestObjectSupported().Return(true) + m.EXPECT().RequestObjectSigningAlgorithmsSupported().Return([]string{"RS256"}) + return m + }()}, + []string{"RS256"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := op.RequestObjectSigAlgorithms(tt.args.c) + assert.Equal(t, tt.want, got) }) } } @@ -244,9 +306,311 @@ func Test_AuthMethodsTokenEndpoint(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := op.AuthMethodsTokenEndpoint(tt.args.c); !reflect.DeepEqual(got, tt.want) { - t.Errorf("authMethods() = %v, want %v", got, tt.want) - } + got := op.AuthMethodsTokenEndpoint(tt.args.c) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_TokenSigAlgorithms(t *testing.T) { + m := mock.NewMockConfiguration(gomock.NewController(t)) + type args struct { + c op.Configuration + } + tests := []struct { + name string + args args + want []string + }{ + { + "not supported, empty", + args{func() op.Configuration { + m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false) + return m + }()}, + nil, + }, + { + "supported, empty", + args{func() op.Configuration { + m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true) + m.EXPECT().TokenEndpointSigningAlgorithmsSupported().Return(nil) + return m + }()}, + nil, + }, + { + "supported, list", + args{func() op.Configuration { + m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true) + m.EXPECT().TokenEndpointSigningAlgorithmsSupported().Return([]string{"RS256"}) + return m + }()}, + []string{"RS256"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := op.TokenSigAlgorithms(tt.args.c) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_IntrospectionSigAlgorithms(t *testing.T) { + m := mock.NewMockConfiguration(gomock.NewController(t)) + type args struct { + c op.Configuration + } + tests := []struct { + name string + args args + want []string + }{ + { + "not supported, empty", + args{func() op.Configuration { + m.EXPECT().IntrospectionAuthMethodPrivateKeyJWTSupported().Return(false) + return m + }()}, + nil, + }, + { + "supported, empty", + args{func() op.Configuration { + m.EXPECT().IntrospectionAuthMethodPrivateKeyJWTSupported().Return(true) + m.EXPECT().IntrospectionEndpointSigningAlgorithmsSupported().Return(nil) + return m + }()}, + nil, + }, + { + "supported, list", + args{func() op.Configuration { + m.EXPECT().IntrospectionAuthMethodPrivateKeyJWTSupported().Return(true) + m.EXPECT().IntrospectionEndpointSigningAlgorithmsSupported().Return([]string{"RS256"}) + return m + }()}, + []string{"RS256"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := op.IntrospectionSigAlgorithms(tt.args.c) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_AuthMethodsIntrospectionEndpoint(t *testing.T) { + type args struct { + c op.Configuration + } + tests := []struct { + name string + args args + want []oidc.AuthMethod + }{ + { + "basic only", + args{func() op.Configuration { + m := mock.NewMockConfiguration(gomock.NewController(t)) + m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false) + return m + }()}, + []oidc.AuthMethod{oidc.AuthMethodBasic}, + }, + { + "basic and private_key_jwt", + args{func() op.Configuration { + m := mock.NewMockConfiguration(gomock.NewController(t)) + m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true) + return m + }()}, + []oidc.AuthMethod{oidc.AuthMethodBasic, oidc.AuthMethodPrivateKeyJWT}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := op.AuthMethodsIntrospectionEndpoint(tt.args.c) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_RevocationSigAlgorithms(t *testing.T) { + m := mock.NewMockConfiguration(gomock.NewController(t)) + type args struct { + c op.Configuration + } + tests := []struct { + name string + args args + want []string + }{ + { + "not supported, empty", + args{func() op.Configuration { + m.EXPECT().RevocationAuthMethodPrivateKeyJWTSupported().Return(false) + return m + }()}, + nil, + }, + { + "supported, empty", + args{func() op.Configuration { + m.EXPECT().RevocationAuthMethodPrivateKeyJWTSupported().Return(true) + m.EXPECT().RevocationEndpointSigningAlgorithmsSupported().Return(nil) + return m + }()}, + nil, + }, + { + "supported, list", + args{func() op.Configuration { + m.EXPECT().RevocationAuthMethodPrivateKeyJWTSupported().Return(true) + m.EXPECT().RevocationEndpointSigningAlgorithmsSupported().Return([]string{"RS256"}) + return m + }()}, + []string{"RS256"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := op.RevocationSigAlgorithms(tt.args.c) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_AuthMethodsRevocationEndpoint(t *testing.T) { + type args struct { + c op.Configuration + } + tests := []struct { + name string + args args + want []oidc.AuthMethod + }{ + { + "none and basic", + args{func() op.Configuration { + m := mock.NewMockConfiguration(gomock.NewController(t)) + m.EXPECT().AuthMethodPostSupported().Return(false) + m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false) + return m + }()}, + []oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic}, + }, + { + "none, basic and post", + args{func() op.Configuration { + m := mock.NewMockConfiguration(gomock.NewController(t)) + m.EXPECT().AuthMethodPostSupported().Return(true) + m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false) + return m + }()}, + []oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost}, + }, + { + "none, basic, post and private_key_jwt", + args{func() op.Configuration { + m := mock.NewMockConfiguration(gomock.NewController(t)) + m.EXPECT().AuthMethodPostSupported().Return(true) + m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true) + return m + }()}, + []oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost, oidc.AuthMethodPrivateKeyJWT}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := op.AuthMethodsRevocationEndpoint(tt.args.c) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestSupportedClaims(t *testing.T) { + type args struct { + c op.Configuration + } + tests := []struct { + name string + args args + want []string + }{ + { + "scopes", + args{}, + []string{ + "sub", + "aud", + "exp", + "iat", + "iss", + "auth_time", + "nonce", + "acr", + "amr", + "c_hash", + "at_hash", + "act", + "scopes", + "client_id", + "azp", + "preferred_username", + "name", + "family_name", + "given_name", + "locale", + "email", + "email_verified", + "phone_number", + "phone_number_verified", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := op.SupportedClaims(tt.args.c) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_CodeChallengeMethods(t *testing.T) { + type args struct { + c op.Configuration + } + tests := []struct { + name string + args args + want []oidc.CodeChallengeMethod + }{ + { + "not supported", + args{func() op.Configuration { + m := mock.NewMockConfiguration(gomock.NewController(t)) + m.EXPECT().CodeMethodS256Supported().Return(false) + return m + }()}, + []oidc.CodeChallengeMethod{}, + }, + { + "S256", + args{func() op.Configuration { + m := mock.NewMockConfiguration(gomock.NewController(t)) + m.EXPECT().CodeMethodS256Supported().Return(true) + return m + }()}, + []oidc.CodeChallengeMethod{oidc.CodeChallengeMethodS256}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := op.CodeChallengeMethods(tt.args.c) + assert.Equal(t, tt.want, got) }) } } diff --git a/pkg/op/endpoint_test.go b/pkg/op/endpoint_test.go index 7c8d1ce..50de89c 100644 --- a/pkg/op/endpoint_test.go +++ b/pkg/op/endpoint_test.go @@ -3,7 +3,7 @@ package op_test import ( "testing" - "github.com/zitadel/oidc/pkg/op" + "github.com/zitadel/oidc/v2/pkg/op" ) func TestEndpoint_Path(t *testing.T) { diff --git a/pkg/op/error.go b/pkg/op/error.go index 3c820d6..acca4ab 100644 --- a/pkg/op/error.go +++ b/pkg/op/error.go @@ -3,8 +3,8 @@ package op import ( "net/http" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) type ErrAuthRequest interface { diff --git a/pkg/op/keys.go b/pkg/op/keys.go index a80211e..239ecbd 100644 --- a/pkg/op/keys.go +++ b/pkg/op/keys.go @@ -6,11 +6,11 @@ import ( "gopkg.in/square/go-jose.v2" - httphelper "github.com/zitadel/oidc/pkg/http" + httphelper "github.com/zitadel/oidc/v2/pkg/http" ) type KeyProvider interface { - GetKeySet(context.Context) (*jose.JSONWebKeySet, error) + KeySet(context.Context) ([]Key, error) } func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) { @@ -20,10 +20,23 @@ func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) { } func Keys(w http.ResponseWriter, r *http.Request, k KeyProvider) { - keySet, err := k.GetKeySet(r.Context()) + keySet, err := k.KeySet(r.Context()) if err != nil { httphelper.MarshalJSONWithStatus(w, err, http.StatusInternalServerError) return } - httphelper.MarshalJSON(w, keySet) + httphelper.MarshalJSON(w, jsonWebKeySet(keySet)) +} + +func jsonWebKeySet(keys []Key) *jose.JSONWebKeySet { + webKeys := make([]jose.JSONWebKey, len(keys)) + for i, key := range keys { + webKeys[i] = jose.JSONWebKey{ + KeyID: key.ID(), + Algorithm: string(key.Algorithm()), + Use: key.Use(), + Key: key.Key(), + } + } + return &jose.JSONWebKeySet{Keys: webKeys} } diff --git a/pkg/op/keys_test.go b/pkg/op/keys_test.go index 7618589..2e56b78 100644 --- a/pkg/op/keys_test.go +++ b/pkg/op/keys_test.go @@ -11,9 +11,9 @@ import ( "github.com/stretchr/testify/assert" "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/pkg/oidc" - "github.com/zitadel/oidc/pkg/op" - "github.com/zitadel/oidc/pkg/op/mock" + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v2/pkg/op/mock" ) func TestKeys(t *testing.T) { @@ -35,7 +35,7 @@ func TestKeys(t *testing.T) { args: args{ k: func() op.KeyProvider { m := mock.NewMockKeyProvider(gomock.NewController(t)) - m.EXPECT().GetKeySet(gomock.Any()).Return(nil, oidc.ErrServerError()) + m.EXPECT().KeySet(gomock.Any()).Return(nil, oidc.ErrServerError()) return m }(), }, @@ -51,39 +51,39 @@ func TestKeys(t *testing.T) { args: args{ k: func() op.KeyProvider { m := mock.NewMockKeyProvider(gomock.NewController(t)) - m.EXPECT().GetKeySet(gomock.Any()).Return(nil, nil) + m.EXPECT().KeySet(gomock.Any()).Return(nil, nil) return m }(), }, res: res{ statusCode: http.StatusOK, contentType: "application/json", + body: `{"keys":[]} +`, }, }, { name: "list", args: args{ k: func() op.KeyProvider { - m := mock.NewMockKeyProvider(gomock.NewController(t)) - m.EXPECT().GetKeySet(gomock.Any()).Return( - &jose.JSONWebKeySet{Keys: []jose.JSONWebKey{ - { - Key: &rsa.PublicKey{ - N: big.NewInt(1), - E: 1, - }, - KeyID: "id", - }, - }}, - nil, - ) + ctrl := gomock.NewController(t) + m := mock.NewMockKeyProvider(ctrl) + k := mock.NewMockKey(ctrl) + k.EXPECT().Key().Return(&rsa.PublicKey{ + N: big.NewInt(1), + E: 1, + }) + k.EXPECT().ID().Return("id") + k.EXPECT().Algorithm().Return(jose.RS256) + k.EXPECT().Use().Return("sig") + m.EXPECT().KeySet(gomock.Any()).Return([]op.Key{k}, nil) return m }(), }, res: res{ statusCode: http.StatusOK, contentType: "application/json", - body: `{"keys":[{"kty":"RSA","kid":"id","n":"AQ","e":"AQ"}]} + body: `{"keys":[{"use":"sig","kty":"RSA","kid":"id","alg":"RS256","n":"AQ","e":"AQ"}]} `, }, }, diff --git a/pkg/op/mock/authorizer.mock.go b/pkg/op/mock/authorizer.mock.go index 52f3877..cc913ee 100644 --- a/pkg/op/mock/authorizer.mock.go +++ b/pkg/op/mock/authorizer.mock.go @@ -1,15 +1,16 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/pkg/op (interfaces: Authorizer) +// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: Authorizer) // Package mock is a generated GoMock package. package mock import ( + context "context" reflect "reflect" gomock "github.com/golang/mock/gomock" - http "github.com/zitadel/oidc/pkg/http" - op "github.com/zitadel/oidc/pkg/op" + http "github.com/zitadel/oidc/v2/pkg/http" + op "github.com/zitadel/oidc/v2/pkg/op" ) // MockAuthorizer is a mock of Authorizer interface. @@ -78,31 +79,17 @@ func (mr *MockAuthorizerMockRecorder) Encoder() *gomock.Call { } // IDTokenHintVerifier mocks base method. -func (m *MockAuthorizer) IDTokenHintVerifier() op.IDTokenHintVerifier { +func (m *MockAuthorizer) IDTokenHintVerifier(arg0 context.Context) op.IDTokenHintVerifier { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IDTokenHintVerifier") + ret := m.ctrl.Call(m, "IDTokenHintVerifier", arg0) ret0, _ := ret[0].(op.IDTokenHintVerifier) return ret0 } // IDTokenHintVerifier indicates an expected call of IDTokenHintVerifier. -func (mr *MockAuthorizerMockRecorder) IDTokenHintVerifier() *gomock.Call { +func (mr *MockAuthorizerMockRecorder) IDTokenHintVerifier(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenHintVerifier", reflect.TypeOf((*MockAuthorizer)(nil).IDTokenHintVerifier)) -} - -// Issuer mocks base method. -func (m *MockAuthorizer) Issuer() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Issuer") - ret0, _ := ret[0].(string) - return ret0 -} - -// Issuer indicates an expected call of Issuer. -func (mr *MockAuthorizerMockRecorder) Issuer() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Issuer", reflect.TypeOf((*MockAuthorizer)(nil).Issuer)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenHintVerifier", reflect.TypeOf((*MockAuthorizer)(nil).IDTokenHintVerifier), arg0) } // RequestObjectSupported mocks base method. @@ -119,20 +106,6 @@ func (mr *MockAuthorizerMockRecorder) RequestObjectSupported() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestObjectSupported", reflect.TypeOf((*MockAuthorizer)(nil).RequestObjectSupported)) } -// Signer mocks base method. -func (m *MockAuthorizer) Signer() op.Signer { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Signer") - ret0, _ := ret[0].(op.Signer) - return ret0 -} - -// Signer indicates an expected call of Signer. -func (mr *MockAuthorizerMockRecorder) Signer() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Signer", reflect.TypeOf((*MockAuthorizer)(nil).Signer)) -} - // Storage mocks base method. func (m *MockAuthorizer) Storage() op.Storage { m.ctrl.T.Helper() diff --git a/pkg/op/mock/authorizer.mock.impl.go b/pkg/op/mock/authorizer.mock.impl.go index d4f29d5..3f1d525 100644 --- a/pkg/op/mock/authorizer.mock.impl.go +++ b/pkg/op/mock/authorizer.mock.impl.go @@ -8,8 +8,8 @@ import ( "github.com/gorilla/schema" "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/pkg/oidc" - "github.com/zitadel/oidc/pkg/op" + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" ) func NewAuthorizer(t *testing.T) op.Authorizer { @@ -20,23 +20,13 @@ func NewAuthorizerExpectValid(t *testing.T, wantErr bool) op.Authorizer { m := NewAuthorizer(t) ExpectDecoder(m) ExpectEncoder(m) - ExpectSigner(m, t) + //ExpectSigner(m, t) ExpectStorage(m, t) ExpectVerifier(m, t) // ExpectErrorHandler(m, t, wantErr) return m } -// func NewAuthorizerExpectDecoderFails(t *testing.T) op.Authorizer { -// m := NewAuthorizer(t) -// ExpectDecoderFails(m) -// ExpectEncoder(m) -// ExpectSigner(m, t) -// ExpectStorage(m, t) -// ExpectErrorHandler(m, t) -// return m -// } - func ExpectDecoder(a op.Authorizer) { mockA := a.(*MockAuthorizer) mockA.EXPECT().Decoder().AnyTimes().Return(schema.NewDecoder()) @@ -47,17 +37,18 @@ func ExpectEncoder(a op.Authorizer) { mockA.EXPECT().Encoder().AnyTimes().Return(schema.NewEncoder()) } -func ExpectSigner(a op.Authorizer, t *testing.T) { - mockA := a.(*MockAuthorizer) - mockA.EXPECT().Signer().DoAndReturn( - func() op.Signer { - return &Sig{} - }) -} +// +//func ExpectSigner(a op.Authorizer, t *testing.T) { +// mockA := a.(*MockAuthorizer) +// mockA.EXPECT().Signer().DoAndReturn( +// func() op.Signer { +// return &Sig{} +// }) +//} func ExpectVerifier(a op.Authorizer, t *testing.T) { mockA := a.(*MockAuthorizer) - mockA.EXPECT().IDTokenHintVerifier().DoAndReturn( + mockA.EXPECT().IDTokenHintVerifier(gomock.Any()).DoAndReturn( func() op.IDTokenHintVerifier { return op.NewIDTokenHintVerifier("", nil) }) diff --git a/pkg/op/mock/client.go b/pkg/op/mock/client.go index 3b16e5e..36df84a 100644 --- a/pkg/op/mock/client.go +++ b/pkg/op/mock/client.go @@ -5,8 +5,8 @@ import ( "github.com/golang/mock/gomock" - "github.com/zitadel/oidc/pkg/oidc" - "github.com/zitadel/oidc/pkg/op" + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" ) func NewClient(t *testing.T) op.Client { diff --git a/pkg/op/mock/client.mock.go b/pkg/op/mock/client.mock.go index cfe3703..e3d19fb 100644 --- a/pkg/op/mock/client.mock.go +++ b/pkg/op/mock/client.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/pkg/op (interfaces: Client) +// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: Client) // Package mock is a generated GoMock package. package mock @@ -9,8 +9,8 @@ import ( time "time" gomock "github.com/golang/mock/gomock" - oidc "github.com/zitadel/oidc/pkg/oidc" - op "github.com/zitadel/oidc/pkg/op" + oidc "github.com/zitadel/oidc/v2/pkg/oidc" + op "github.com/zitadel/oidc/v2/pkg/op" ) // MockClient is a mock of Client interface. diff --git a/pkg/op/mock/configuration.mock.go b/pkg/op/mock/configuration.mock.go index e0c90dc..fc3158a 100644 --- a/pkg/op/mock/configuration.mock.go +++ b/pkg/op/mock/configuration.mock.go @@ -1,14 +1,15 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/pkg/op (interfaces: Configuration) +// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: Configuration) // Package mock is a generated GoMock package. package mock import ( + http "net/http" reflect "reflect" gomock "github.com/golang/mock/gomock" - op "github.com/zitadel/oidc/pkg/op" + op "github.com/zitadel/oidc/v2/pkg/op" language "golang.org/x/text/language" ) @@ -161,6 +162,20 @@ func (mr *MockConfigurationMockRecorder) GrantTypeTokenExchangeSupported() *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeTokenExchangeSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeTokenExchangeSupported)) } +// Insecure mocks base method. +func (m *MockConfiguration) Insecure() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Insecure") + ret0, _ := ret[0].(bool) + return ret0 +} + +// Insecure indicates an expected call of Insecure. +func (mr *MockConfigurationMockRecorder) Insecure() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insecure", reflect.TypeOf((*MockConfiguration)(nil).Insecure)) +} + // IntrospectionAuthMethodPrivateKeyJWTSupported mocks base method. func (m *MockConfiguration) IntrospectionAuthMethodPrivateKeyJWTSupported() bool { m.ctrl.T.Helper() @@ -203,18 +218,18 @@ func (mr *MockConfigurationMockRecorder) IntrospectionEndpointSigningAlgorithmsS return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntrospectionEndpointSigningAlgorithmsSupported", reflect.TypeOf((*MockConfiguration)(nil).IntrospectionEndpointSigningAlgorithmsSupported)) } -// Issuer mocks base method. -func (m *MockConfiguration) Issuer() string { +// IssuerFromRequest mocks base method. +func (m *MockConfiguration) IssuerFromRequest(arg0 *http.Request) string { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Issuer") + ret := m.ctrl.Call(m, "IssuerFromRequest", arg0) ret0, _ := ret[0].(string) return ret0 } -// Issuer indicates an expected call of Issuer. -func (mr *MockConfigurationMockRecorder) Issuer() *gomock.Call { +// IssuerFromRequest indicates an expected call of IssuerFromRequest. +func (mr *MockConfigurationMockRecorder) IssuerFromRequest(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Issuer", reflect.TypeOf((*MockConfiguration)(nil).Issuer)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IssuerFromRequest", reflect.TypeOf((*MockConfiguration)(nil).IssuerFromRequest), arg0) } // KeysEndpoint mocks base method. diff --git a/pkg/op/mock/discovery.mock.go b/pkg/op/mock/discovery.mock.go new file mode 100644 index 0000000..0c78d52 --- /dev/null +++ b/pkg/op/mock/discovery.mock.go @@ -0,0 +1,51 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: DiscoverStorage) + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + jose "gopkg.in/square/go-jose.v2" +) + +// MockDiscoverStorage is a mock of DiscoverStorage interface. +type MockDiscoverStorage struct { + ctrl *gomock.Controller + recorder *MockDiscoverStorageMockRecorder +} + +// MockDiscoverStorageMockRecorder is the mock recorder for MockDiscoverStorage. +type MockDiscoverStorageMockRecorder struct { + mock *MockDiscoverStorage +} + +// NewMockDiscoverStorage creates a new mock instance. +func NewMockDiscoverStorage(ctrl *gomock.Controller) *MockDiscoverStorage { + mock := &MockDiscoverStorage{ctrl: ctrl} + mock.recorder = &MockDiscoverStorageMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDiscoverStorage) EXPECT() *MockDiscoverStorageMockRecorder { + return m.recorder +} + +// SignatureAlgorithms mocks base method. +func (m *MockDiscoverStorage) SignatureAlgorithms(arg0 context.Context) ([]jose.SignatureAlgorithm, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SignatureAlgorithms", arg0) + ret0, _ := ret[0].([]jose.SignatureAlgorithm) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SignatureAlgorithms indicates an expected call of SignatureAlgorithms. +func (mr *MockDiscoverStorageMockRecorder) SignatureAlgorithms(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignatureAlgorithms", reflect.TypeOf((*MockDiscoverStorage)(nil).SignatureAlgorithms), arg0) +} diff --git a/pkg/op/mock/generate.go b/pkg/op/mock/generate.go index c9c7efa..0066571 100644 --- a/pkg/op/mock/generate.go +++ b/pkg/op/mock/generate.go @@ -1,8 +1,9 @@ package mock -//go:generate mockgen -package mock -destination ./storage.mock.go github.com/zitadel/oidc/pkg/op Storage -//go:generate mockgen -package mock -destination ./authorizer.mock.go github.com/zitadel/oidc/pkg/op Authorizer -//go:generate mockgen -package mock -destination ./client.mock.go github.com/zitadel/oidc/pkg/op Client -//go:generate mockgen -package mock -destination ./configuration.mock.go github.com/zitadel/oidc/pkg/op Configuration -//go:generate mockgen -package mock -destination ./signer.mock.go github.com/zitadel/oidc/pkg/op Signer -//go:generate mockgen -package mock -destination ./key.mock.go github.com/zitadel/oidc/pkg/op KeyProvider +//go:generate mockgen -package mock -destination ./storage.mock.go github.com/zitadel/oidc/v2/pkg/op Storage +//go:generate mockgen -package mock -destination ./authorizer.mock.go github.com/zitadel/oidc/v2/pkg/op Authorizer +//go:generate mockgen -package mock -destination ./client.mock.go github.com/zitadel/oidc/v2/pkg/op Client +//go:generate mockgen -package mock -destination ./configuration.mock.go github.com/zitadel/oidc/v2/pkg/op Configuration +//go:generate mockgen -package mock -destination ./discovery.mock.go github.com/zitadel/oidc/v2/pkg/op DiscoverStorage +//go:generate mockgen -package mock -destination ./signer.mock.go github.com/zitadel/oidc/v2/pkg/op SigningKey,Key +//go:generate mockgen -package mock -destination ./key.mock.go github.com/zitadel/oidc/v2/pkg/op KeyProvider diff --git a/pkg/op/mock/key.mock.go b/pkg/op/mock/key.mock.go index 56d12dc..8831651 100644 --- a/pkg/op/mock/key.mock.go +++ b/pkg/op/mock/key.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/pkg/op (interfaces: KeyProvider) +// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: KeyProvider) // Package mock is a generated GoMock package. package mock @@ -9,7 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - jose "gopkg.in/square/go-jose.v2" + op "github.com/zitadel/oidc/v2/pkg/op" ) // MockKeyProvider is a mock of KeyProvider interface. @@ -35,17 +35,17 @@ func (m *MockKeyProvider) EXPECT() *MockKeyProviderMockRecorder { return m.recorder } -// GetKeySet mocks base method. -func (m *MockKeyProvider) GetKeySet(arg0 context.Context) (*jose.JSONWebKeySet, error) { +// KeySet mocks base method. +func (m *MockKeyProvider) KeySet(arg0 context.Context) ([]op.Key, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetKeySet", arg0) - ret0, _ := ret[0].(*jose.JSONWebKeySet) + ret := m.ctrl.Call(m, "KeySet", arg0) + ret0, _ := ret[0].([]op.Key) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetKeySet indicates an expected call of GetKeySet. -func (mr *MockKeyProviderMockRecorder) GetKeySet(arg0 interface{}) *gomock.Call { +// KeySet indicates an expected call of KeySet. +func (mr *MockKeyProviderMockRecorder) KeySet(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeySet", reflect.TypeOf((*MockKeyProvider)(nil).GetKeySet), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeySet", reflect.TypeOf((*MockKeyProvider)(nil).KeySet), arg0) } diff --git a/pkg/op/mock/signer.mock.go b/pkg/op/mock/signer.mock.go index 42a92fb..78c0efe 100644 --- a/pkg/op/mock/signer.mock.go +++ b/pkg/op/mock/signer.mock.go @@ -1,56 +1,69 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/pkg/op (interfaces: Signer) +// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: SigningKey,Key) // Package mock is a generated GoMock package. package mock import ( - context "context" reflect "reflect" gomock "github.com/golang/mock/gomock" jose "gopkg.in/square/go-jose.v2" ) -// MockSigner is a mock of Signer interface. -type MockSigner struct { +// MockSigningKey is a mock of SigningKey interface. +type MockSigningKey struct { ctrl *gomock.Controller - recorder *MockSignerMockRecorder + recorder *MockSigningKeyMockRecorder } -// MockSignerMockRecorder is the mock recorder for MockSigner. -type MockSignerMockRecorder struct { - mock *MockSigner +// MockSigningKeyMockRecorder is the mock recorder for MockSigningKey. +type MockSigningKeyMockRecorder struct { + mock *MockSigningKey } -// NewMockSigner creates a new mock instance. -func NewMockSigner(ctrl *gomock.Controller) *MockSigner { - mock := &MockSigner{ctrl: ctrl} - mock.recorder = &MockSignerMockRecorder{mock} +// NewMockSigningKey creates a new mock instance. +func NewMockSigningKey(ctrl *gomock.Controller) *MockSigningKey { + mock := &MockSigningKey{ctrl: ctrl} + mock.recorder = &MockSigningKeyMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockSigner) EXPECT() *MockSignerMockRecorder { +func (m *MockSigningKey) EXPECT() *MockSigningKeyMockRecorder { return m.recorder } -// Health mocks base method. -func (m *MockSigner) Health(arg0 context.Context) error { +// ID mocks base method. +func (m *MockSigningKey) ID() string { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Health", arg0) - ret0, _ := ret[0].(error) + ret := m.ctrl.Call(m, "ID") + ret0, _ := ret[0].(string) return ret0 } -// Health indicates an expected call of Health. -func (mr *MockSignerMockRecorder) Health(arg0 interface{}) *gomock.Call { +// ID indicates an expected call of ID. +func (mr *MockSigningKeyMockRecorder) ID() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockSigner)(nil).Health), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ID", reflect.TypeOf((*MockSigningKey)(nil).ID)) +} + +// Key mocks base method. +func (m *MockSigningKey) Key() interface{} { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Key") + ret0, _ := ret[0].(interface{}) + return ret0 +} + +// Key indicates an expected call of Key. +func (mr *MockSigningKeyMockRecorder) Key() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Key", reflect.TypeOf((*MockSigningKey)(nil).Key)) } // SignatureAlgorithm mocks base method. -func (m *MockSigner) SignatureAlgorithm() jose.SignatureAlgorithm { +func (m *MockSigningKey) SignatureAlgorithm() jose.SignatureAlgorithm { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SignatureAlgorithm") ret0, _ := ret[0].(jose.SignatureAlgorithm) @@ -58,21 +71,86 @@ func (m *MockSigner) SignatureAlgorithm() jose.SignatureAlgorithm { } // SignatureAlgorithm indicates an expected call of SignatureAlgorithm. -func (mr *MockSignerMockRecorder) SignatureAlgorithm() *gomock.Call { +func (mr *MockSigningKeyMockRecorder) SignatureAlgorithm() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignatureAlgorithm", reflect.TypeOf((*MockSigner)(nil).SignatureAlgorithm)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignatureAlgorithm", reflect.TypeOf((*MockSigningKey)(nil).SignatureAlgorithm)) } -// Signer mocks base method. -func (m *MockSigner) Signer() jose.Signer { +// MockKey is a mock of Key interface. +type MockKey struct { + ctrl *gomock.Controller + recorder *MockKeyMockRecorder +} + +// MockKeyMockRecorder is the mock recorder for MockKey. +type MockKeyMockRecorder struct { + mock *MockKey +} + +// NewMockKey creates a new mock instance. +func NewMockKey(ctrl *gomock.Controller) *MockKey { + mock := &MockKey{ctrl: ctrl} + mock.recorder = &MockKeyMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockKey) EXPECT() *MockKeyMockRecorder { + return m.recorder +} + +// Algorithm mocks base method. +func (m *MockKey) Algorithm() jose.SignatureAlgorithm { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Signer") - ret0, _ := ret[0].(jose.Signer) + ret := m.ctrl.Call(m, "Algorithm") + ret0, _ := ret[0].(jose.SignatureAlgorithm) return ret0 } -// Signer indicates an expected call of Signer. -func (mr *MockSignerMockRecorder) Signer() *gomock.Call { +// Algorithm indicates an expected call of Algorithm. +func (mr *MockKeyMockRecorder) Algorithm() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Signer", reflect.TypeOf((*MockSigner)(nil).Signer)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Algorithm", reflect.TypeOf((*MockKey)(nil).Algorithm)) +} + +// ID mocks base method. +func (m *MockKey) ID() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ID") + ret0, _ := ret[0].(string) + return ret0 +} + +// ID indicates an expected call of ID. +func (mr *MockKeyMockRecorder) ID() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ID", reflect.TypeOf((*MockKey)(nil).ID)) +} + +// Key mocks base method. +func (m *MockKey) Key() interface{} { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Key") + ret0, _ := ret[0].(interface{}) + return ret0 +} + +// Key indicates an expected call of Key. +func (mr *MockKeyMockRecorder) Key() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Key", reflect.TypeOf((*MockKey)(nil).Key)) +} + +// Use mocks base method. +func (m *MockKey) Use() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Use") + ret0, _ := ret[0].(string) + return ret0 +} + +// Use indicates an expected call of Use. +func (mr *MockKeyMockRecorder) Use() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Use", reflect.TypeOf((*MockKey)(nil).Use)) } diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index 785a643..58cc2a0 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/pkg/op (interfaces: Storage) +// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: Storage) // Package mock is a generated GoMock package. package mock @@ -10,8 +10,8 @@ import ( time "time" gomock "github.com/golang/mock/gomock" - oidc "github.com/zitadel/oidc/pkg/oidc" - op "github.com/zitadel/oidc/pkg/op" + oidc "github.com/zitadel/oidc/v2/pkg/oidc" + op "github.com/zitadel/oidc/v2/pkg/op" jose "gopkg.in/square/go-jose.v2" ) @@ -174,21 +174,6 @@ func (mr *MockStorageMockRecorder) GetKeyByIDAndUserID(arg0, arg1, arg2 interfac return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeyByIDAndUserID", reflect.TypeOf((*MockStorage)(nil).GetKeyByIDAndUserID), arg0, arg1, arg2) } -// GetKeySet mocks base method. -func (m *MockStorage) GetKeySet(arg0 context.Context) (*jose.JSONWebKeySet, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetKeySet", arg0) - ret0, _ := ret[0].(*jose.JSONWebKeySet) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetKeySet indicates an expected call of GetKeySet. -func (mr *MockStorageMockRecorder) GetKeySet(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeySet", reflect.TypeOf((*MockStorage)(nil).GetKeySet), arg0) -} - // GetPrivateClaimsFromScopes mocks base method. func (m *MockStorage) GetPrivateClaimsFromScopes(arg0 context.Context, arg1, arg2 string, arg3 []string) (map[string]interface{}, error) { m.ctrl.T.Helper() @@ -204,18 +189,6 @@ func (mr *MockStorageMockRecorder) GetPrivateClaimsFromScopes(arg0, arg1, arg2, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrivateClaimsFromScopes", reflect.TypeOf((*MockStorage)(nil).GetPrivateClaimsFromScopes), arg0, arg1, arg2, arg3) } -// GetSigningKey mocks base method. -func (m *MockStorage) GetSigningKey(arg0 context.Context, arg1 chan<- jose.SigningKey) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "GetSigningKey", arg0, arg1) -} - -// GetSigningKey indicates an expected call of GetSigningKey. -func (mr *MockStorageMockRecorder) GetSigningKey(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSigningKey", reflect.TypeOf((*MockStorage)(nil).GetSigningKey), arg0, arg1) -} - // Health mocks base method. func (m *MockStorage) Health(arg0 context.Context) error { m.ctrl.T.Helper() @@ -230,6 +203,21 @@ func (mr *MockStorageMockRecorder) Health(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockStorage)(nil).Health), arg0) } +// KeySet mocks base method. +func (m *MockStorage) KeySet(arg0 context.Context) ([]op.Key, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "KeySet", arg0) + ret0, _ := ret[0].([]op.Key) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// KeySet indicates an expected call of KeySet. +func (mr *MockStorageMockRecorder) KeySet(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeySet", reflect.TypeOf((*MockStorage)(nil).KeySet), arg0) +} + // RevokeToken mocks base method. func (m *MockStorage) RevokeToken(arg0 context.Context, arg1, arg2, arg3 string) *oidc.Error { m.ctrl.T.Helper() @@ -300,6 +288,36 @@ func (mr *MockStorageMockRecorder) SetUserinfoFromToken(arg0, arg1, arg2, arg3, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserinfoFromToken", reflect.TypeOf((*MockStorage)(nil).SetUserinfoFromToken), arg0, arg1, arg2, arg3, arg4) } +// SignatureAlgorithms mocks base method. +func (m *MockStorage) SignatureAlgorithms(arg0 context.Context) ([]jose.SignatureAlgorithm, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SignatureAlgorithms", arg0) + ret0, _ := ret[0].([]jose.SignatureAlgorithm) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SignatureAlgorithms indicates an expected call of SignatureAlgorithms. +func (mr *MockStorageMockRecorder) SignatureAlgorithms(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignatureAlgorithms", reflect.TypeOf((*MockStorage)(nil).SignatureAlgorithms), arg0) +} + +// SigningKey mocks base method. +func (m *MockStorage) SigningKey(arg0 context.Context) (op.SigningKey, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SigningKey", arg0) + ret0, _ := ret[0].(op.SigningKey) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SigningKey indicates an expected call of SigningKey. +func (mr *MockStorageMockRecorder) SigningKey(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SigningKey", reflect.TypeOf((*MockStorage)(nil).SigningKey), arg0) +} + // TerminateSession mocks base method. func (m *MockStorage) TerminateSession(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() diff --git a/pkg/op/mock/storage.mock.impl.go b/pkg/op/mock/storage.mock.impl.go index 946cee0..9269f89 100644 --- a/pkg/op/mock/storage.mock.impl.go +++ b/pkg/op/mock/storage.mock.impl.go @@ -6,13 +6,10 @@ import ( "testing" "time" - "github.com/zitadel/oidc/pkg/oidc" - - "gopkg.in/square/go-jose.v2" - "github.com/golang/mock/gomock" - "github.com/zitadel/oidc/pkg/op" + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" ) func NewStorage(t *testing.T) op.Storage { @@ -41,13 +38,13 @@ func NewMockStorageAny(t *testing.T) op.Storage { func NewMockStorageSigningKeyInvalid(t *testing.T) op.Storage { m := NewStorage(t) - ExpectSigningKeyInvalid(m) + //ExpectSigningKeyInvalid(m) return m } func NewMockStorageSigningKey(t *testing.T) op.Storage { m := NewStorage(t) - ExpectSigningKey(m) + //ExpectSigningKey(m) return m } @@ -85,24 +82,6 @@ func ExpectValidClientID(s op.Storage) { }) } -func ExpectSigningKeyInvalid(s op.Storage) { - mockS := s.(*MockStorage) - mockS.EXPECT().GetSigningKey(gomock.Any(), gomock.Any()).DoAndReturn( - func(_ context.Context, keyCh chan<- jose.SigningKey) { - keyCh <- jose.SigningKey{} - }, - ) -} - -func ExpectSigningKey(s op.Storage) { - mockS := s.(*MockStorage) - mockS.EXPECT().GetSigningKey(gomock.Any(), gomock.Any()).DoAndReturn( - func(_ context.Context, keyCh chan<- jose.SigningKey) { - keyCh <- jose.SigningKey{Algorithm: jose.HS256, Key: []byte("key")} - }, - ) -} - type ConfClient struct { id string appType op.ApplicationType diff --git a/pkg/op/op.go b/pkg/op/op.go index d85dcd6..acedcb6 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -12,8 +12,8 @@ import ( "golang.org/x/text/language" "gopkg.in/square/go-jose.v2" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) const ( @@ -29,78 +29,79 @@ const ( defaultKeysEndpoint = "keys" ) -var DefaultEndpoints = &endpoints{ - Authorization: NewEndpoint(defaultAuthorizationEndpoint), - Token: NewEndpoint(defaultTokenEndpoint), - Introspection: NewEndpoint(defaultIntrospectEndpoint), - Userinfo: NewEndpoint(defaultUserinfoEndpoint), - Revocation: NewEndpoint(defaultRevocationEndpoint), - EndSession: NewEndpoint(defaultEndSessionEndpoint), - JwksURI: NewEndpoint(defaultKeysEndpoint), -} +var ( + DefaultEndpoints = &endpoints{ + Authorization: NewEndpoint(defaultAuthorizationEndpoint), + Token: NewEndpoint(defaultTokenEndpoint), + Introspection: NewEndpoint(defaultIntrospectEndpoint), + Userinfo: NewEndpoint(defaultUserinfoEndpoint), + Revocation: NewEndpoint(defaultRevocationEndpoint), + EndSession: NewEndpoint(defaultEndSessionEndpoint), + JwksURI: NewEndpoint(defaultKeysEndpoint), + } + + defaultCORSOptions = cors.Options{ + AllowCredentials: true, + AllowedHeaders: []string{ + "Origin", + "Accept", + "Accept-Language", + "Authorization", + "Content-Type", + "X-Requested-With", + }, + AllowedMethods: []string{ + http.MethodGet, + http.MethodHead, + http.MethodPost, + }, + ExposedHeaders: []string{ + "Location", + "Content-Length", + }, + AllowOriginFunc: func(_ string) bool { + return true + }, + } +) type OpenIDProvider interface { Configuration Storage() Storage Decoder() httphelper.Decoder Encoder() httphelper.Encoder - IDTokenHintVerifier() IDTokenHintVerifier - AccessTokenVerifier() AccessTokenVerifier + IDTokenHintVerifier(context.Context) IDTokenHintVerifier + AccessTokenVerifier(context.Context) AccessTokenVerifier Crypto() Crypto DefaultLogoutRedirectURI() string - Signer() Signer Probes() []ProbesFn HttpHandler() http.Handler } type HttpInterceptor func(http.Handler) http.Handler -var defaultCORSOptions = cors.Options{ - AllowCredentials: true, - AllowedHeaders: []string{ - "Origin", - "Accept", - "Accept-Language", - "Authorization", - "Content-Type", - "X-Requested-With", - }, - AllowedMethods: []string{ - http.MethodGet, - http.MethodHead, - http.MethodPost, - }, - ExposedHeaders: []string{ - "Location", - "Content-Length", - }, - AllowOriginFunc: func(_ string) bool { - return true - }, -} - func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router { - intercept := buildInterceptor(interceptors...) router := mux.NewRouter() router.Use(cors.New(defaultCORSOptions).Handler) + router.Use(intercept(o.IssuerFromRequest, interceptors...)) router.HandleFunc(healthEndpoint, healthHandler) router.HandleFunc(readinessEndpoint, readyHandler(o.Probes())) - router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Signer())) - router.Handle(o.AuthorizationEndpoint().Relative(), intercept(authorizeHandler(o))) - router.NewRoute().Path(authCallbackPath(o)).Queries("id", "{id}").Handler(intercept(authorizeCallbackHandler(o))) - router.Handle(o.TokenEndpoint().Relative(), intercept(tokenHandler(o))) + router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Storage())) + router.HandleFunc(o.AuthorizationEndpoint().Relative(), authorizeHandler(o)) + router.NewRoute().Path(authCallbackPath(o)).Queries("id", "{id}").HandlerFunc(authorizeCallbackHandler(o)) + router.HandleFunc(o.TokenEndpoint().Relative(), tokenHandler(o)) router.HandleFunc(o.IntrospectionEndpoint().Relative(), introspectionHandler(o)) router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o)) router.HandleFunc(o.RevocationEndpoint().Relative(), revocationHandler(o)) - router.Handle(o.EndSessionEndpoint().Relative(), intercept(endSessionHandler(o))) + router.HandleFunc(o.EndSessionEndpoint().Relative(), endSessionHandler(o)) router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o.Storage())) return router } // AuthCallbackURL builds the url for the redirect (with the requestID) after a successful login -func AuthCallbackURL(o OpenIDProvider) func(string) string { - return func(requestID string) string { - return o.AuthorizationEndpoint().Absolute(o.Issuer()) + authCallbackPathSuffix + "?id=" + requestID +func AuthCallbackURL(o OpenIDProvider) func(context.Context, string) string { + return func(ctx context.Context, requestID string) string { + return o.AuthorizationEndpoint().Absolute(IssuerFromContext(ctx)) + authCallbackPathSuffix + "?id=" + requestID } } @@ -109,7 +110,6 @@ func authCallbackPath(o OpenIDProvider) string { } type Config struct { - Issuer string CryptoKey [32]byte DefaultLogoutRedirectURI string CodeMethodS256 bool @@ -133,29 +133,34 @@ type endpoints struct { // NewOpenIDProvider creates a provider. The provider provides (with HttpHandler()) // a http.Router that handles a suite of endpoints (some paths can be overridden): -// /healthz -// /ready -// /.well-known/openid-configuration -// /oauth/token -// /oauth/introspect -// /callback -// /authorize -// /userinfo -// /revoke -// /end_session -// /keys +// +// /healthz +// /ready +// /.well-known/openid-configuration +// /oauth/token +// /oauth/introspect +// /callback +// /authorize +// /userinfo +// /revoke +// /end_session +// /keys +// // This does not include login. Login is handled with a redirect that includes the // request ID. The redirect for logins is specified per-client by Client.LoginURL(). // Successful logins should mark the request as authorized and redirect back to to // op.AuthCallbackURL(provider) which is probably /callback. On the redirect back // to the AuthCallbackURL, the request id should be passed as the "id" parameter. -func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opOpts ...Option) (OpenIDProvider, error) { - err := ValidateIssuer(config.Issuer) - if err != nil { - return nil, err - } +func NewOpenIDProvider(ctx context.Context, issuer string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) { + return newProvider(ctx, config, storage, StaticIssuer(issuer), opOpts...) +} - o := &openidProvider{ +func NewDynamicOpenIDProvider(ctx context.Context, path string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) { + return newProvider(ctx, config, storage, IssuerFromHost(path), opOpts...) +} + +func newProvider(ctx context.Context, config *Config, storage Storage, issuer func(bool) (IssuerFromRequest, error), opOpts ...Option) (_ *Provider, err error) { + o := &Provider{ config: config, storage: storage, endpoints: DefaultEndpoints, @@ -168,9 +173,10 @@ func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opO } } - keyCh := make(chan jose.SigningKey) - go storage.GetSigningKey(ctx, keyCh) - o.signer = NewSigner(ctx, storage, keyCh) + o.issuer, err = issuer(o.insecure) + if err != nil { + return nil, err + } o.httpHandler = CreateRouter(o, o.interceptors...) @@ -182,22 +188,17 @@ func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opO o.crypto = NewAESCrypto(config.CryptoKey) // Avoid potential race conditions by calling these early - _ = o.AccessTokenVerifier() // sets accessTokenVerifier - _ = o.IDTokenHintVerifier() // sets idTokenHintVerifier - _ = o.JWTProfileVerifier() // sets jwtProfileVerifier - _ = o.openIDKeySet() // sets keySet + _ = o.openIDKeySet() // sets keySet return o, nil } -type openidProvider struct { +type Provider struct { config *Config + issuer IssuerFromRequest + insecure bool endpoints *endpoints storage Storage - signer Signer - idTokenHintVerifier IDTokenHintVerifier - jwtProfileVerifier JWTProfileVerifier - accessTokenVerifier AccessTokenVerifier keySet *openIDKeySet crypto Crypto httpHandler http.Handler @@ -209,159 +210,149 @@ type openidProvider struct { idTokenHintVerifierOpts []IDTokenHintVerifierOpt } -func (o *openidProvider) Issuer() string { - return o.config.Issuer +func (o *Provider) IssuerFromRequest(r *http.Request) string { + return o.issuer(r) } -func (o *openidProvider) AuthorizationEndpoint() Endpoint { +func (o *Provider) Insecure() bool { + return o.insecure +} + +func (o *Provider) AuthorizationEndpoint() Endpoint { return o.endpoints.Authorization } -func (o *openidProvider) TokenEndpoint() Endpoint { +func (o *Provider) TokenEndpoint() Endpoint { return o.endpoints.Token } -func (o *openidProvider) IntrospectionEndpoint() Endpoint { +func (o *Provider) IntrospectionEndpoint() Endpoint { return o.endpoints.Introspection } -func (o *openidProvider) UserinfoEndpoint() Endpoint { +func (o *Provider) UserinfoEndpoint() Endpoint { return o.endpoints.Userinfo } -func (o *openidProvider) RevocationEndpoint() Endpoint { +func (o *Provider) RevocationEndpoint() Endpoint { return o.endpoints.Revocation } -func (o *openidProvider) EndSessionEndpoint() Endpoint { +func (o *Provider) EndSessionEndpoint() Endpoint { return o.endpoints.EndSession } -func (o *openidProvider) KeysEndpoint() Endpoint { +func (o *Provider) KeysEndpoint() Endpoint { return o.endpoints.JwksURI } -func (o *openidProvider) AuthMethodPostSupported() bool { +func (o *Provider) AuthMethodPostSupported() bool { return o.config.AuthMethodPost } -func (o *openidProvider) CodeMethodS256Supported() bool { +func (o *Provider) CodeMethodS256Supported() bool { return o.config.CodeMethodS256 } -func (o *openidProvider) AuthMethodPrivateKeyJWTSupported() bool { +func (o *Provider) AuthMethodPrivateKeyJWTSupported() bool { return o.config.AuthMethodPrivateKeyJWT } -func (o *openidProvider) TokenEndpointSigningAlgorithmsSupported() []string { +func (o *Provider) TokenEndpointSigningAlgorithmsSupported() []string { return []string{"RS256"} } -func (o *openidProvider) GrantTypeRefreshTokenSupported() bool { +func (o *Provider) GrantTypeRefreshTokenSupported() bool { return o.config.GrantTypeRefreshToken } -func (o *openidProvider) GrantTypeTokenExchangeSupported() bool { +func (o *Provider) GrantTypeTokenExchangeSupported() bool { return false } -func (o *openidProvider) GrantTypeJWTAuthorizationSupported() bool { +func (o *Provider) GrantTypeJWTAuthorizationSupported() bool { return true } -func (o *openidProvider) GrantTypeClientCredentialsSupported() bool { +func (o *Provider) IntrospectionAuthMethodPrivateKeyJWTSupported() bool { + return true +} + +func (o *Provider) IntrospectionEndpointSigningAlgorithmsSupported() []string { + return []string{"RS256"} +} + +func (o *Provider) GrantTypeClientCredentialsSupported() bool { _, ok := o.storage.(ClientCredentialsStorage) return ok } -func (o *openidProvider) IntrospectionAuthMethodPrivateKeyJWTSupported() bool { +func (o *Provider) RevocationAuthMethodPrivateKeyJWTSupported() bool { return true } -func (o *openidProvider) IntrospectionEndpointSigningAlgorithmsSupported() []string { +func (o *Provider) RevocationEndpointSigningAlgorithmsSupported() []string { return []string{"RS256"} } -func (o *openidProvider) RevocationAuthMethodPrivateKeyJWTSupported() bool { - return true -} - -func (o *openidProvider) RevocationEndpointSigningAlgorithmsSupported() []string { - return []string{"RS256"} -} - -func (o *openidProvider) RequestObjectSupported() bool { +func (o *Provider) RequestObjectSupported() bool { return o.config.RequestObjectSupported } -func (o *openidProvider) RequestObjectSigningAlgorithmsSupported() []string { +func (o *Provider) RequestObjectSigningAlgorithmsSupported() []string { return []string{"RS256"} } -func (o *openidProvider) SupportedUILocales() []language.Tag { +func (o *Provider) SupportedUILocales() []language.Tag { return o.config.SupportedUILocales } -func (o *openidProvider) Storage() Storage { +func (o *Provider) Storage() Storage { return o.storage } -func (o *openidProvider) Decoder() httphelper.Decoder { +func (o *Provider) Decoder() httphelper.Decoder { return o.decoder } -func (o *openidProvider) Encoder() httphelper.Encoder { +func (o *Provider) Encoder() httphelper.Encoder { return o.encoder } -func (o *openidProvider) IDTokenHintVerifier() IDTokenHintVerifier { - if o.idTokenHintVerifier == nil { - o.idTokenHintVerifier = NewIDTokenHintVerifier(o.Issuer(), o.openIDKeySet(), o.idTokenHintVerifierOpts...) - } - return o.idTokenHintVerifier +func (o *Provider) IDTokenHintVerifier(ctx context.Context) IDTokenHintVerifier { + return NewIDTokenHintVerifier(IssuerFromContext(ctx), o.openIDKeySet(), o.idTokenHintVerifierOpts...) } -func (o *openidProvider) JWTProfileVerifier() JWTProfileVerifier { - if o.jwtProfileVerifier == nil { - o.jwtProfileVerifier = NewJWTProfileVerifier(o.Storage(), o.Issuer(), 1*time.Hour, time.Second) - } - return o.jwtProfileVerifier +func (o *Provider) JWTProfileVerifier(ctx context.Context) JWTProfileVerifier { + return NewJWTProfileVerifier(o.Storage(), IssuerFromContext(ctx), 1*time.Hour, time.Second) } -func (o *openidProvider) AccessTokenVerifier() AccessTokenVerifier { - if o.accessTokenVerifier == nil { - o.accessTokenVerifier = NewAccessTokenVerifier(o.Issuer(), o.openIDKeySet(), o.accessTokenVerifierOpts...) - } - return o.accessTokenVerifier +func (o *Provider) AccessTokenVerifier(ctx context.Context) AccessTokenVerifier { + return NewAccessTokenVerifier(IssuerFromContext(ctx), o.openIDKeySet(), o.accessTokenVerifierOpts...) } -func (o *openidProvider) openIDKeySet() oidc.KeySet { +func (o *Provider) openIDKeySet() oidc.KeySet { if o.keySet == nil { o.keySet = &openIDKeySet{o.Storage()} } return o.keySet } -func (o *openidProvider) Crypto() Crypto { +func (o *Provider) Crypto() Crypto { return o.crypto } -func (o *openidProvider) DefaultLogoutRedirectURI() string { +func (o *Provider) DefaultLogoutRedirectURI() string { return o.config.DefaultLogoutRedirectURI } -func (o *openidProvider) Signer() Signer { - return o.signer -} - -func (o *openidProvider) Probes() []ProbesFn { +func (o *Provider) Probes() []ProbesFn { return []ProbesFn{ - ReadySigner(o.Signer()), ReadyStorage(o.Storage()), } } -func (o *openidProvider) HttpHandler() http.Handler { +func (o *Provider) HttpHandler() http.Handler { return o.httpHandler } @@ -372,22 +363,31 @@ type openIDKeySet struct { // VerifySignature implements the oidc.KeySet interface // providing an implementation for the keys stored in the OP Storage interface func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { - keySet, err := o.Storage.GetKeySet(ctx) + keySet, err := o.Storage.KeySet(ctx) if err != nil { return nil, fmt.Errorf("error fetching keys: %w", err) } keyID, alg := oidc.GetKeyIDAndAlg(jws) - key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, keySet.Keys...) + key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, jsonWebKeySet(keySet).Keys...) if err != nil { return nil, fmt.Errorf("invalid signature: %w", err) } return jws.Verify(&key) } -type Option func(o *openidProvider) error +type Option func(o *Provider) error + +// WithAllowInsecure allows the use of http (instead of https) for issuers +// this is not recommended for production use and violates the OIDC specification +func WithAllowInsecure() Option { + return func(o *Provider) error { + o.insecure = true + return nil + } +} func WithCustomAuthEndpoint(endpoint Endpoint) Option { - return func(o *openidProvider) error { + return func(o *Provider) error { if err := endpoint.Validate(); err != nil { return err } @@ -397,7 +397,7 @@ func WithCustomAuthEndpoint(endpoint Endpoint) Option { } func WithCustomTokenEndpoint(endpoint Endpoint) Option { - return func(o *openidProvider) error { + return func(o *Provider) error { if err := endpoint.Validate(); err != nil { return err } @@ -407,7 +407,7 @@ func WithCustomTokenEndpoint(endpoint Endpoint) Option { } func WithCustomIntrospectionEndpoint(endpoint Endpoint) Option { - return func(o *openidProvider) error { + return func(o *Provider) error { if err := endpoint.Validate(); err != nil { return err } @@ -417,7 +417,7 @@ func WithCustomIntrospectionEndpoint(endpoint Endpoint) Option { } func WithCustomUserinfoEndpoint(endpoint Endpoint) Option { - return func(o *openidProvider) error { + return func(o *Provider) error { if err := endpoint.Validate(); err != nil { return err } @@ -427,7 +427,7 @@ func WithCustomUserinfoEndpoint(endpoint Endpoint) Option { } func WithCustomRevocationEndpoint(endpoint Endpoint) Option { - return func(o *openidProvider) error { + return func(o *Provider) error { if err := endpoint.Validate(); err != nil { return err } @@ -437,7 +437,7 @@ func WithCustomRevocationEndpoint(endpoint Endpoint) Option { } func WithCustomEndSessionEndpoint(endpoint Endpoint) Option { - return func(o *openidProvider) error { + return func(o *Provider) error { if err := endpoint.Validate(); err != nil { return err } @@ -447,7 +447,7 @@ func WithCustomEndSessionEndpoint(endpoint Endpoint) Option { } func WithCustomKeysEndpoint(endpoint Endpoint) Option { - return func(o *openidProvider) error { + return func(o *Provider) error { if err := endpoint.Validate(); err != nil { return err } @@ -457,7 +457,7 @@ func WithCustomKeysEndpoint(endpoint Endpoint) Option { } func WithCustomEndpoints(auth, token, userInfo, revocation, endSession, keys Endpoint) Option { - return func(o *openidProvider) error { + return func(o *Provider) error { o.endpoints.Authorization = auth o.endpoints.Token = token o.endpoints.Userinfo = userInfo @@ -469,38 +469,32 @@ func WithCustomEndpoints(auth, token, userInfo, revocation, endSession, keys End } func WithHttpInterceptors(interceptors ...HttpInterceptor) Option { - return func(o *openidProvider) error { + return func(o *Provider) error { o.interceptors = append(o.interceptors, interceptors...) return nil } } func WithAccessTokenVerifierOpts(opts ...AccessTokenVerifierOpt) Option { - return func(o *openidProvider) error { + return func(o *Provider) error { o.accessTokenVerifierOpts = opts return nil } } func WithIDTokenHintVerifierOpts(opts ...IDTokenHintVerifierOpt) Option { - return func(o *openidProvider) error { + return func(o *Provider) error { o.idTokenHintVerifierOpts = opts return nil } } -func buildInterceptor(interceptors ...HttpInterceptor) func(http.HandlerFunc) http.Handler { - return func(handlerFunc http.HandlerFunc) http.Handler { - handler := handlerFuncToHandler(handlerFunc) +func intercept(i IssuerFromRequest, interceptors ...HttpInterceptor) func(handler http.Handler) http.Handler { + issuerInterceptor := NewIssuerInterceptor(i) + return func(handler http.Handler) http.Handler { for i := len(interceptors) - 1; i >= 0; i-- { handler = interceptors[i](handler) } - return handler + return cors.New(defaultCORSOptions).Handler(issuerInterceptor.Handler(handler)) } } - -func handlerFuncToHandler(handlerFunc http.HandlerFunc) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handlerFunc(w, r) - }) -} diff --git a/pkg/op/probes.go b/pkg/op/probes.go index 7b80fb4..a56c92b 100644 --- a/pkg/op/probes.go +++ b/pkg/op/probes.go @@ -5,7 +5,7 @@ import ( "errors" "net/http" - httphelper "github.com/zitadel/oidc/pkg/http" + httphelper "github.com/zitadel/oidc/v2/pkg/http" ) type ProbesFn func(context.Context) error @@ -31,15 +31,6 @@ func Readiness(w http.ResponseWriter, r *http.Request, probes ...ProbesFn) { ok(w) } -func ReadySigner(s Signer) ProbesFn { - return func(ctx context.Context) error { - if s == nil { - return errors.New("no signer") - } - return s.Health(ctx) - } -} - func ReadyStorage(s Storage) ProbesFn { return func(ctx context.Context) error { if s == nil { diff --git a/pkg/op/session.go b/pkg/op/session.go index c4984fc..e1cc595 100644 --- a/pkg/op/session.go +++ b/pkg/op/session.go @@ -5,14 +5,14 @@ import ( "net/http" "net/url" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) type SessionEnder interface { Decoder() httphelper.Decoder Storage() Storage - IDTokenHintVerifier() IDTokenHintVerifier + IDTokenHintVerifier(context.Context) IDTokenHintVerifier DefaultLogoutRedirectURI() string } @@ -59,7 +59,7 @@ func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest, RedirectURI: ender.DefaultLogoutRedirectURI(), } if req.IdTokenHint != "" { - claims, err := VerifyIDTokenHint(ctx, req.IdTokenHint, ender.IDTokenHintVerifier()) + claims, err := VerifyIDTokenHint(ctx, req.IdTokenHint, ender.IDTokenHintVerifier(ctx)) if err != nil { return nil, oidc.ErrInvalidRequest().WithDescription("id_token_hint invalid").WithParent(err) } diff --git a/pkg/op/signer.go b/pkg/op/signer.go index 828876e..22ef8ca 100644 --- a/pkg/op/signer.go +++ b/pkg/op/signer.go @@ -1,88 +1,38 @@ package op import ( - "context" "errors" - "sync" - "github.com/zitadel/logging" "gopkg.in/square/go-jose.v2" ) -type Signer interface { - Health(ctx context.Context) error - Signer() jose.Signer +var ( + ErrSignerCreationFailed = errors.New("signer creation failed") +) + +type SigningKey interface { SignatureAlgorithm() jose.SignatureAlgorithm + Key() interface{} + ID() string } -type tokenSigner struct { - signer jose.Signer - storage AuthStorage - alg jose.SignatureAlgorithm - lock sync.RWMutex -} - -func NewSigner(ctx context.Context, storage AuthStorage, keyCh <-chan jose.SigningKey) Signer { - s := &tokenSigner{ - storage: storage, - } - - select { - case <-ctx.Done(): - return nil - case key := <-keyCh: - s.exchangeSigningKey(key) - } - go s.refreshSigningKey(ctx, keyCh) - - return s -} - -func (s *tokenSigner) Health(_ context.Context) error { - if s.signer == nil { - return errors.New("no signer") - } - if string(s.alg) == "" { - return errors.New("no signing algorithm") - } - return nil -} - -func (s *tokenSigner) Signer() jose.Signer { - s.lock.RLock() - defer s.lock.RUnlock() - return s.signer -} - -func (s *tokenSigner) refreshSigningKey(ctx context.Context, keyCh <-chan jose.SigningKey) { - for { - select { - case <-ctx.Done(): - return - case key := <-keyCh: - s.exchangeSigningKey(key) - } - } -} - -func (s *tokenSigner) exchangeSigningKey(key jose.SigningKey) { - s.lock.Lock() - defer s.lock.Unlock() - s.alg = key.Algorithm - if key.Algorithm == "" || key.Key == nil { - s.signer = nil - logging.Warn("signer has no key") - return - } - var err error - s.signer, err = jose.NewSigner(key, &jose.SignerOptions{}) +func SignerFromKey(key SigningKey) (jose.Signer, error) { + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: key.SignatureAlgorithm(), + Key: &jose.JSONWebKey{ + Key: key.Key(), + KeyID: key.ID(), + }, + }, &jose.SignerOptions{}) if err != nil { - logging.New().WithError(err).Error("error creating signer") - return + return nil, ErrSignerCreationFailed //TODO: log / wrap error? } - logging.Info("signer exchanged signing key") + return signer, nil } -func (s *tokenSigner) SignatureAlgorithm() jose.SignatureAlgorithm { - return s.alg +type Key interface { + ID() string + Algorithm() jose.SignatureAlgorithm + Use() string + Key() interface{} } diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 153cd21..b040b72 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -7,7 +7,7 @@ import ( "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/oidc" ) type AuthStorage interface { @@ -47,8 +47,14 @@ type AuthStorage interface { // tokenOrTokenID will be the refresh token, not its ID. RevokeToken(ctx context.Context, tokenOrTokenID string, userID string, clientID string) *oidc.Error - GetSigningKey(context.Context, chan<- jose.SigningKey) - GetKeySet(context.Context) (*jose.JSONWebKeySet, error) + SigningKey(context.Context) (SigningKey, error) + SignatureAlgorithms(context.Context) ([]jose.SignatureAlgorithm, error) + KeySet(context.Context) ([]Key, error) +} + +type ClientCredentialsStorage interface { + ClientCredentials(ctx context.Context, clientID, clientSecret string) (Client, error) + ClientCredentialsTokenRequest(ctx context.Context, clientID string, scopes []string) (TokenRequest, error) } // CanRefreshTokenInfo is an optional additional interface that Storage can support. @@ -62,10 +68,6 @@ type CanRefreshTokenInfo interface { var ErrInvalidRefreshToken = errors.New("invalid_refresh_token") -type ClientCredentialsStorage interface { - ClientCredentialsTokenRequest(ctx context.Context, clientID string, scopes []string) (TokenRequest, error) -} - type OPStorage interface { GetClientByClientID(ctx context.Context, clientID string) (Client, error) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error @@ -80,6 +82,12 @@ type OPStorage interface { ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error) } +// JWTProfileTokenStorage is an additional, optional storage to implement +// implementing it, allows specifying the [AccessTokenType] of the access_token returned form the JWT Profile TokenRequest +type JWTProfileTokenStorage interface { + JWTProfileTokenType(ctx context.Context, request TokenRequest) (AccessTokenType, error) +} + // Storage is a required parameter for NewOpenIDProvider(). In addition to the // embedded interfaces below, if the passed Storage implements ClientCredentialsStorage // then the grant type "client_credentials" will be supported. In that case, the access diff --git a/pkg/op/token.go b/pkg/op/token.go index 3a72261..4d3e620 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -4,14 +4,12 @@ import ( "context" "time" - "github.com/zitadel/oidc/pkg/crypto" - "github.com/zitadel/oidc/pkg/oidc" - "github.com/zitadel/oidc/pkg/strings" + "github.com/zitadel/oidc/v2/pkg/crypto" + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/strings" ) type TokenCreator interface { - Issuer() string - Signer() Signer Storage() Storage Crypto() Crypto } @@ -22,6 +20,13 @@ type TokenRequest interface { GetScopes() []string } +type AccessTokenClient interface { + GetID() string + ClockSkew() time.Duration + RestrictAdditionalAccessTokenScopes() func(scopes []string) []string + GrantTypes() []oidc.GrantType +} + func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Client, creator TokenCreator, createAccessToken bool, code, refreshToken string) (*oidc.AccessTokenResponse, error) { var accessToken, newRefreshToken string var validity time.Duration @@ -32,7 +37,7 @@ func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Cli return nil, err } } - idToken, err := CreateIDToken(ctx, creator.Issuer(), request, client.IDTokenLifetime(), accessToken, code, creator.Storage(), creator.Signer(), client) + idToken, err := CreateIDToken(ctx, IssuerFromContext(ctx), request, client.IDTokenLifetime(), accessToken, code, creator.Storage(), client) if err != nil { return nil, err } @@ -57,7 +62,7 @@ func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Cli }, nil } -func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storage, refreshToken string, client Client) (id, newRefreshToken string, exp time.Time, err error) { +func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storage, refreshToken string, client AccessTokenClient) (id, newRefreshToken string, exp time.Time, err error) { if needsRefreshToken(tokenRequest, client) { return storage.CreateAccessAndRefreshTokens(ctx, tokenRequest, refreshToken) } @@ -65,7 +70,7 @@ func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storag return } -func needsRefreshToken(tokenRequest TokenRequest, client Client) bool { +func needsRefreshToken(tokenRequest TokenRequest, client AccessTokenClient) bool { switch req := tokenRequest.(type) { case AuthRequest: return strings.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && req.GetResponseType() == oidc.ResponseTypeCode && ValidateGrantType(client, oidc.GrantTypeRefreshToken) @@ -76,7 +81,7 @@ func needsRefreshToken(tokenRequest TokenRequest, client Client) bool { } } -func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTokenType AccessTokenType, creator TokenCreator, client Client, refreshToken string) (accessToken, newRefreshToken string, validity time.Duration, err error) { +func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTokenType AccessTokenType, creator TokenCreator, client AccessTokenClient, refreshToken string) (accessToken, newRefreshToken string, validity time.Duration, err error) { id, newRefreshToken, exp, err := createTokens(ctx, tokenRequest, creator.Storage(), refreshToken, client) if err != nil { return "", "", 0, err @@ -87,7 +92,7 @@ func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTok } validity = exp.Add(clockSkew).Sub(time.Now().UTC()) if accessTokenType == AccessTokenTypeJWT { - accessToken, err = CreateJWT(ctx, creator.Issuer(), tokenRequest, exp, id, creator.Signer(), client, creator.Storage()) + accessToken, err = CreateJWT(ctx, IssuerFromContext(ctx), tokenRequest, exp, id, client, creator.Storage()) return } accessToken, err = CreateBearerToken(id, tokenRequest.GetSubject(), creator.Crypto()) @@ -98,7 +103,7 @@ func CreateBearerToken(tokenID, subject string, crypto Crypto) (string, error) { return crypto.Encrypt(tokenID + ":" + subject) } -func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, exp time.Time, id string, signer Signer, client Client, storage Storage) (string, error) { +func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, exp time.Time, id string, client AccessTokenClient, storage Storage) (string, error) { claims := oidc.NewAccessTokenClaims(issuer, tokenRequest.GetSubject(), tokenRequest.GetAudience(), exp, id, client.GetID(), client.ClockSkew()) if client != nil { restrictedScopes := client.RestrictAdditionalAccessTokenScopes()(tokenRequest.GetScopes()) @@ -108,7 +113,15 @@ func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, ex } claims.SetPrivateClaims(privateClaims) } - return crypto.Sign(claims, signer.Signer()) + signingKey, err := storage.SigningKey(ctx) + if err != nil { + return "", err + } + signer, err := SignerFromKey(signingKey) + if err != nil { + return "", err + } + return crypto.Sign(claims, signer) } type IDTokenRequest interface { @@ -120,7 +133,7 @@ type IDTokenRequest interface { GetSubject() string } -func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, validity time.Duration, accessToken, code string, storage Storage, signer Signer, client Client) (string, error) { +func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, validity time.Duration, accessToken, code string, storage Storage, client Client) (string, error) { exp := time.Now().UTC().Add(client.ClockSkew()).Add(validity) var acr, nonce string if authRequest, ok := request.(AuthRequest); ok { @@ -129,8 +142,12 @@ func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, v } claims := oidc.NewIDTokenClaims(issuer, request.GetSubject(), request.GetAudience(), exp, request.GetAuthTime(), nonce, acr, request.GetAMR(), request.GetClientID(), client.ClockSkew()) scopes := client.RestrictAdditionalIdTokenScopes()(request.GetScopes()) + signingKey, err := storage.SigningKey(ctx) + if err != nil { + return "", err + } if accessToken != "" { - atHash, err := oidc.ClaimHash(accessToken, signer.SignatureAlgorithm()) + atHash, err := oidc.ClaimHash(accessToken, signingKey.SignatureAlgorithm()) if err != nil { return "", err } @@ -148,14 +165,17 @@ func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, v claims.SetUserinfo(userInfo) } if code != "" { - codeHash, err := oidc.ClaimHash(code, signer.SignatureAlgorithm()) + codeHash, err := oidc.ClaimHash(code, signingKey.SignatureAlgorithm()) if err != nil { return "", err } claims.SetCodeHash(codeHash) } - - return crypto.Sign(claims, signer.Signer()) + signer, err := SignerFromKey(signingKey) + if err != nil { + return "", err + } + return crypto.Sign(claims, signer) } func removeUserinfoScopes(scopes []string) []string { diff --git a/pkg/op/token_client_credentials.go b/pkg/op/token_client_credentials.go index 3787667..fc31d57 100644 --- a/pkg/op/token_client_credentials.go +++ b/pkg/op/token_client_credentials.go @@ -5,8 +5,8 @@ import ( "net/http" "net/url" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) // ClientCredentialsExchange handles the OAuth 2.0 client_credentials grant, including @@ -63,15 +63,15 @@ func ParseClientCredentialsRequest(r *http.Request, decoder httphelper.Decoder) return request, nil } -// ValidateClientCredentialsRequest validates the refresh_token request parameters including authorization check of the client -// and returns the data representing the original auth request corresponding to the refresh_token +// ValidateClientCredentialsRequest validates the client_credentials request parameters including authorization check of the client +// and returns a TokenRequest and Client implementation to be used in the client_credentials response, resp. creation of the corresponding access_token. func ValidateClientCredentialsRequest(ctx context.Context, request *oidc.ClientCredentialsRequest, exchanger Exchanger) (TokenRequest, Client, error) { storage, ok := exchanger.Storage().(ClientCredentialsStorage) if !ok { return nil, nil, oidc.ErrUnsupportedGrantType().WithDescription("client_credentials grant not supported") } - client, err := AuthorizeClientCredentialsClient(ctx, request, exchanger) + client, err := AuthorizeClientCredentialsClient(ctx, request, storage) if err != nil { return nil, nil, err } @@ -84,12 +84,8 @@ func ValidateClientCredentialsRequest(ctx context.Context, request *oidc.ClientC return tokenRequest, client, nil } -func AuthorizeClientCredentialsClient(ctx context.Context, request *oidc.ClientCredentialsRequest, exchanger Exchanger) (Client, error) { - if err := AuthorizeClientIDSecret(ctx, request.ClientID, request.ClientSecret, exchanger.Storage()); err != nil { - return nil, err - } - - client, err := exchanger.Storage().GetClientByClientID(ctx, request.ClientID) +func AuthorizeClientCredentialsClient(ctx context.Context, request *oidc.ClientCredentialsRequest, storage ClientCredentialsStorage) (Client, error) { + client, err := storage.ClientCredentials(ctx, request.ClientID, request.ClientSecret) if err != nil { return nil, oidc.ErrInvalidClient().WithParent(err) } @@ -102,7 +98,7 @@ func AuthorizeClientCredentialsClient(ctx context.Context, request *oidc.ClientC } func CreateClientCredentialsTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator, client Client) (*oidc.AccessTokenResponse, error) { - accessToken, _, validity, err := CreateAccessToken(ctx, tokenRequest, AccessTokenTypeJWT, creator, client, "") + accessToken, _, validity, err := CreateAccessToken(ctx, tokenRequest, client.AccessTokenType(), creator, client, "") if err != nil { return nil, err } diff --git a/pkg/op/token_code.go b/pkg/op/token_code.go index ec48233..565a477 100644 --- a/pkg/op/token_code.go +++ b/pkg/op/token_code.go @@ -4,8 +4,8 @@ import ( "context" "net/http" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) // CodeExchange handles the OAuth 2.0 authorization_code grant, including diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go index f402c8b..dfc8954 100644 --- a/pkg/op/token_intospection.go +++ b/pkg/op/token_intospection.go @@ -1,24 +1,25 @@ package op import ( + "context" "errors" "net/http" "net/url" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) type Introspector interface { Decoder() httphelper.Decoder Crypto() Crypto Storage() Storage - AccessTokenVerifier() AccessTokenVerifier + AccessTokenVerifier(context.Context) AccessTokenVerifier } type IntrospectorJWTProfile interface { Introspector - JWTProfileVerifier() JWTProfileVerifier + JWTProfileVerifier(context.Context) JWTProfileVerifier } func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *http.Request) { @@ -62,7 +63,7 @@ func ParseTokenIntrospectionRequest(r *http.Request, introspector Introspector) return "", "", errors.New("unable to parse request") } if introspectorJWTProfile, ok := introspector.(IntrospectorJWTProfile); ok && req.ClientAssertion != "" { - profile, err := VerifyJWTAssertion(r.Context(), req.ClientAssertion, introspectorJWTProfile.JWTProfileVerifier()) + profile, err := VerifyJWTAssertion(r.Context(), req.ClientAssertion, introspectorJWTProfile.JWTProfileVerifier(r.Context())) if err == nil { return req.Token, profile.Issuer, nil } diff --git a/pkg/op/token_jwt_profile.go b/pkg/op/token_jwt_profile.go index eb21517..23bac9a 100644 --- a/pkg/op/token_jwt_profile.go +++ b/pkg/op/token_jwt_profile.go @@ -5,13 +5,13 @@ import ( "net/http" "time" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) type JWTAuthorizationGrantExchanger interface { Exchanger - JWTProfileVerifier() JWTProfileVerifier + JWTProfileVerifier(context.Context) JWTProfileVerifier } // JWTProfile handles the OAuth 2.0 JWT Profile Authorization Grant https://tools.ietf.org/html/rfc7523#section-2.1 @@ -21,7 +21,7 @@ func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizati RequestError(w, r, err) } - tokenRequest, err := VerifyJWTAssertion(r.Context(), profileRequest.Assertion, exchanger.JWTProfileVerifier()) + tokenRequest, err := VerifyJWTAssertion(r.Context(), profileRequest.Assertion, exchanger.JWTProfileVerifier(r.Context())) if err != nil { RequestError(w, r, err) return @@ -53,27 +53,65 @@ func ParseJWTProfileGrantRequest(r *http.Request, decoder httphelper.Decoder) (* return tokenReq, nil } -// CreateJWTTokenResponse creates +// CreateJWTTokenResponse creates an access_token response for a JWT Profile Grant request +// by default the access_token is an opaque string, but can be specified by implementing the JWTProfileTokenStorage interface func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator) (*oidc.AccessTokenResponse, error) { - id, exp, err := creator.Storage().CreateAccessToken(ctx, tokenRequest) - if err != nil { - return nil, err - } - accessToken, err := CreateBearerToken(id, tokenRequest.GetSubject(), creator.Crypto()) - if err != nil { - return nil, err + // return an opaque token as default to not break current implementations + tokenType := AccessTokenTypeBearer + + // the current CreateAccessToken function, esp. CreateJWT requires an implementation of an AccessTokenClient + client := &jwtProfileClient{ + id: tokenRequest.GetSubject(), } + // by implementing the JWTProfileTokenStorage the storage can specify the AccessTokenType to be returned + tokenStorage, ok := creator.Storage().(JWTProfileTokenStorage) + if ok { + var err error + tokenType, err = tokenStorage.JWTProfileTokenType(ctx, tokenRequest) + if err != nil { + return nil, err + } + } + + accessToken, _, validity, err := CreateAccessToken(ctx, tokenRequest, tokenType, creator, client, "") + if err != nil { + return nil, err + } return &oidc.AccessTokenResponse{ AccessToken: accessToken, TokenType: oidc.BearerToken, - ExpiresIn: uint64(exp.Sub(time.Now().UTC()).Seconds()), + ExpiresIn: uint64(validity.Seconds()), }, nil } // ParseJWTProfileRequest has been renamed to ParseJWTProfileGrantRequest // -//deprecated: use ParseJWTProfileGrantRequest +// deprecated: use ParseJWTProfileGrantRequest func ParseJWTProfileRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.JWTProfileGrantRequest, error) { return ParseJWTProfileGrantRequest(r, decoder) } + +type jwtProfileClient struct { + id string +} + +func (j *jwtProfileClient) GetID() string { + return j.id +} + +func (j *jwtProfileClient) ClockSkew() time.Duration { + return 0 +} + +func (j *jwtProfileClient) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { + return func(scopes []string) []string { + return scopes + } +} + +func (j *jwtProfileClient) GrantTypes() []oidc.GrantType { + return []oidc.GrantType{ + oidc.GrantTypeBearer, + } +} diff --git a/pkg/op/token_refresh.go b/pkg/op/token_refresh.go index 7251eeb..148d2a4 100644 --- a/pkg/op/token_refresh.go +++ b/pkg/op/token_refresh.go @@ -6,9 +6,9 @@ import ( "net/http" "time" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" - "github.com/zitadel/oidc/pkg/strings" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/strings" ) type RefreshTokenRequest interface { diff --git a/pkg/op/token_request.go b/pkg/op/token_request.go index 6ccd489..190e812 100644 --- a/pkg/op/token_request.go +++ b/pkg/op/token_request.go @@ -5,15 +5,13 @@ import ( "net/http" "net/url" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) type Exchanger interface { - Issuer() string Storage() Storage Decoder() httphelper.Decoder - Signer() Signer Crypto() Crypto AuthMethodPostSupported() bool AuthMethodPrivateKeyJWTSupported() bool @@ -122,7 +120,7 @@ func AuthorizeCodeChallenge(tokenReq *oidc.AccessTokenRequest, challenge *oidc.C // AuthorizePrivateJWTKey authorizes a client by validating the client_assertion's signature with a previously // registered public key (JWT Profile) func AuthorizePrivateJWTKey(ctx context.Context, clientAssertion string, exchanger JWTAuthorizationGrantExchanger) (Client, error) { - jwtReq, err := VerifyJWTAssertion(ctx, clientAssertion, exchanger.JWTProfileVerifier()) + jwtReq, err := VerifyJWTAssertion(ctx, clientAssertion, exchanger.JWTProfileVerifier(ctx)) if err != nil { return nil, err } @@ -136,8 +134,8 @@ func AuthorizePrivateJWTKey(ctx context.Context, clientAssertion string, exchang return client, nil } -// ValidateGrantType ensures that the requested grant_type is allowed by the Client -func ValidateGrantType(client Client, grantType oidc.GrantType) bool { +// ValidateGrantType ensures that the requested grant_type is allowed by the client +func ValidateGrantType(client interface{ GrantTypes() []oidc.GrantType }, grantType oidc.GrantType) bool { if client == nil { return false } diff --git a/pkg/op/token_revocation.go b/pkg/op/token_revocation.go index 9dd0295..7dbd4a7 100644 --- a/pkg/op/token_revocation.go +++ b/pkg/op/token_revocation.go @@ -7,22 +7,22 @@ import ( "net/url" "strings" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) type Revoker interface { Decoder() httphelper.Decoder Crypto() Crypto Storage() Storage - AccessTokenVerifier() AccessTokenVerifier + AccessTokenVerifier(context.Context) AccessTokenVerifier AuthMethodPrivateKeyJWTSupported() bool AuthMethodPostSupported() bool } type RevokerJWTProfile interface { Revoker - JWTProfileVerifier() JWTProfileVerifier + JWTProfileVerifier(context.Context) JWTProfileVerifier } func revocationHandler(revoker Revoker) func(http.ResponseWriter, *http.Request) { @@ -87,7 +87,7 @@ func ParseTokenRevocationRequest(r *http.Request, revoker Revoker) (token, token if !ok || !revoker.AuthMethodPrivateKeyJWTSupported() { return "", "", "", oidc.ErrInvalidClient().WithDescription("auth_method private_key_jwt not supported") } - profile, err := VerifyJWTAssertion(r.Context(), req.ClientAssertion, revokerJWTProfile.JWTProfileVerifier()) + profile, err := VerifyJWTAssertion(r.Context(), req.ClientAssertion, revokerJWTProfile.JWTProfileVerifier(r.Context())) if err == nil { return req.Token, req.TokenTypeHint, profile.Issuer, nil } @@ -151,7 +151,7 @@ func getTokenIDAndSubjectForRevocation(ctx context.Context, userinfoProvider Use } return splitToken[0], splitToken[1], true } - accessTokenClaims, err := VerifyAccessToken(ctx, accessToken, userinfoProvider.AccessTokenVerifier()) + accessTokenClaims, err := VerifyAccessToken(ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx)) if err != nil { return "", "", false } diff --git a/pkg/op/userinfo.go b/pkg/op/userinfo.go index 4bd03e2..cb8f0ae 100644 --- a/pkg/op/userinfo.go +++ b/pkg/op/userinfo.go @@ -6,15 +6,15 @@ import ( "net/http" "strings" - httphelper "github.com/zitadel/oidc/pkg/http" - "github.com/zitadel/oidc/pkg/oidc" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) type UserinfoProvider interface { Decoder() httphelper.Decoder Crypto() Crypto Storage() Storage - AccessTokenVerifier() AccessTokenVerifier + AccessTokenVerifier(context.Context) AccessTokenVerifier } func userinfoHandler(userinfoProvider UserinfoProvider) func(http.ResponseWriter, *http.Request) { @@ -81,7 +81,7 @@ func getTokenIDAndSubject(ctx context.Context, userinfoProvider UserinfoProvider } return splitToken[0], splitToken[1], true } - accessTokenClaims, err := VerifyAccessToken(ctx, accessToken, userinfoProvider.AccessTokenVerifier()) + accessTokenClaims, err := VerifyAccessToken(ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx)) if err != nil { return "", "", false } diff --git a/pkg/op/verifier_access_token.go b/pkg/op/verifier_access_token.go index 1729c23..1d53adb 100644 --- a/pkg/op/verifier_access_token.go +++ b/pkg/op/verifier_access_token.go @@ -4,7 +4,7 @@ import ( "context" "time" - "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/oidc" ) type AccessTokenVerifier interface { diff --git a/pkg/op/verifier_id_token_hint.go b/pkg/op/verifier_id_token_hint.go index d36bbd8..9320106 100644 --- a/pkg/op/verifier_id_token_hint.go +++ b/pkg/op/verifier_id_token_hint.go @@ -4,7 +4,7 @@ import ( "context" "time" - "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/oidc" ) type IDTokenHintVerifier interface { @@ -73,7 +73,7 @@ func NewIDTokenHintVerifier(issuer string, keySet oidc.KeySet, opts ...IDTokenHi } // VerifyIDTokenHint validates the id token according to -//https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation +// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation func VerifyIDTokenHint(ctx context.Context, token string, v IDTokenHintVerifier) (oidc.IDTokenClaims, error) { claims := oidc.EmptyIDTokenClaims() diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index 0215e84..9befb64 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -8,7 +8,7 @@ import ( "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/oidc" ) type JWTProfileVerifier interface { From ff2729cb23d2673f4539057271e98ad092109240 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Feb 2023 11:18:18 +0200 Subject: [PATCH 171/502] chore(deps): bump golang.org/x/text from 0.6.0 to 0.7.0 (#279) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.6.0 to 0.7.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.6.0...v0.7.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1895c24..7c40e39 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/stretchr/testify v1.8.1 github.com/zitadel/logging v0.3.4 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 - golang.org/x/text v0.6.0 + golang.org/x/text v0.7.0 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/square/go-jose.v2 v2.6.0 ) diff --git a/go.sum b/go.sum index 5f3ac4b..d5e856f 100644 --- a/go.sum +++ b/go.sum @@ -285,8 +285,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 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= From c8d61c0858de6b503e38afd8798437b916044963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Mon, 13 Feb 2023 11:28:46 +0200 Subject: [PATCH 172/502] rp: allow to set custom URL parameters (#273) * rp: allow to set prompts in AuthURLHandler Fixes #241 * rp: configuration for handlers with URL options to call RS Fixes #265 --- example/client/app/app.go | 10 +++-- pkg/client/rp/integration_test.go | 17 +++++++- pkg/client/rp/relying_party.go | 66 ++++++++++++++++++++++++------- 3 files changed, 73 insertions(+), 20 deletions(-) diff --git a/example/client/app/app.go b/example/client/app/app.go index b7f2868..e7be491 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -54,10 +54,12 @@ func main() { return uuid.New().String() } - // register the AuthURLHandler at your preferred path - // the AuthURLHandler creates the auth request and redirects the user to the auth server - // including state handling with secure cookie and the possibility to use PKCE - http.Handle("/login", rp.AuthURLHandler(state, provider)) + // register the AuthURLHandler at your preferred path. + // the AuthURLHandler creates the auth request and redirects the user to the auth server. + // including state handling with secure cookie and the possibility to use PKCE. + // Prompts can optionally be set to inform the server of + // any messages that need to be prompted back to the user. + http.Handle("/login", rp.AuthURLHandler(state, provider, rp.WithPromptURLParam("Welcome back!"))) // for demonstration purposes the returned userinfo response is written as JSON object onto response marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) { diff --git a/pkg/client/rp/integration_test.go b/pkg/client/rp/integration_test.go index 6f5f489..732a4bf 100644 --- a/pkg/client/rp/integration_test.go +++ b/pkg/client/rp/integration_test.go @@ -75,7 +75,10 @@ func TestRelyingPartySession(t *testing.T) { state := "state-" + strconv.FormatInt(seed.Int63(), 25) capturedW := httptest.NewRecorder() get := httptest.NewRequest("GET", localURL.String(), nil) - rp.AuthURLHandler(func() string { return state }, provider)(capturedW, get) + rp.AuthURLHandler(func() string { return state }, provider, + rp.WithPromptURLParam("Hello, World!", "Goodbye, World!"), + rp.WithURLParam("custom", "param"), + )(capturedW, get) defer func() { if t.Failed() { @@ -84,6 +87,8 @@ func TestRelyingPartySession(t *testing.T) { }() require.GreaterOrEqual(t, capturedW.Code, 200, "captured response code") require.Less(t, capturedW.Code, 400, "captured response code") + require.Contains(t, capturedW.Body.String(), `prompt=Hello%2C+World%21+Goodbye%2C+World%21`) + require.Contains(t, capturedW.Body.String(), `custom=param`) //nolint:bodyclose resp := capturedW.Result() @@ -140,7 +145,7 @@ func TestRelyingPartySession(t *testing.T) { email = info.GetEmail() http.Redirect(w, r, targetURL, 302) } - rp.CodeExchangeHandler(rp.UserinfoCallback(redirect), provider)(capturedW, get) + rp.CodeExchangeHandler(rp.UserinfoCallback(redirect), provider, rp.WithURLParam("custom", "param"))(capturedW, get) defer func() { if t.Failed() { @@ -150,6 +155,7 @@ func TestRelyingPartySession(t *testing.T) { }() require.Less(t, capturedW.Code, 400, "token exchange response code") require.Less(t, capturedW.Code, 400, "token exchange response code") + // TODO: how to check the custom header was sent to the server? //nolint:bodyclose resp = capturedW.Result() @@ -193,6 +199,13 @@ func TestRelyingPartySession(t *testing.T) { _, err = rp.RefreshAccessToken(provider, newTokens.RefreshToken, "", "") assert.Errorf(t, err, "refresh with replacement") } + + t.Run("WithPrompt", func(t *testing.T) { + opts := rp.WithPrompt("foo", "bar")() + url := provider.OAuthConfig().AuthCodeURL("some", opts...) + + require.Contains(t, url, "prompt=foo+bar") + }) } type deferredHandler struct { diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 86b65da..3758601 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -255,7 +255,7 @@ func WithVerifierOpts(opts ...VerifierOption) Option { // WithClientKey specifies the path to the key.json to be used for the JWT Profile Client Authentication on the token endpoint // -//deprecated: use WithJWTProfile(SignerFromKeyPath(path)) instead +// deprecated: use WithJWTProfile(SignerFromKeyPath(path)) instead func WithClientKey(path string) Option { return WithJWTProfile(SignerFromKeyPath(path)) } @@ -304,7 +304,7 @@ func SignerFromKeyAndKeyID(key []byte, keyID string) SignerFromKey { // Discover calls the discovery endpoint of the provided issuer and returns the found endpoints // -//deprecated: use client.Discover +// deprecated: use client.Discover func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint req, err := http.NewRequest("GET", wellKnown, nil) @@ -323,7 +323,7 @@ func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { } // AuthURL returns the auth request url -//(wrapping the oauth2 `AuthCodeURL`) +// (wrapping the oauth2 `AuthCodeURL`) func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string { authOpts := make([]oauth2.AuthCodeOption, 0) for _, opt := range opts { @@ -333,10 +333,15 @@ func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string { } // AuthURLHandler extends the `AuthURL` method with a http redirect handler -// including handling setting cookie for secure `state` transfer -func AuthURLHandler(stateFn func() string, rp RelyingParty) http.HandlerFunc { +// including handling setting cookie for secure `state` transfer. +// Custom paramaters can optionally be set to the redirect URL. +func AuthURLHandler(stateFn func() string, rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - opts := make([]AuthURLOpt, 0) + opts := make([]AuthURLOpt, len(urlParam)) + for i, p := range urlParam { + opts[i] = AuthURLOpt(p) + } + state := stateFn() if err := trySetStateCookie(w, state, rp); err != nil { http.Error(w, "failed to create state cookie: "+err.Error(), http.StatusUnauthorized) @@ -350,6 +355,7 @@ func AuthURLHandler(stateFn func() string, rp RelyingParty) http.HandlerFunc { } opts = append(opts, WithCodeChallenge(codeChallenge)) } + http.Redirect(w, r, AuthURL(state, rp, opts...), http.StatusFound) } } @@ -398,8 +404,9 @@ type CodeExchangeCallback func(w http.ResponseWriter, r *http.Request, tokens *o // CodeExchangeHandler extends the `CodeExchange` method with a http handler // including cookie handling for secure `state` transfer -// and optional PKCE code verifier checking -func CodeExchangeHandler(callback CodeExchangeCallback, rp RelyingParty) http.HandlerFunc { +// and optional PKCE code verifier checking. +// Custom paramaters can optionally be set to the token URL. +func CodeExchangeHandler(callback CodeExchangeCallback, rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { state, err := tryReadStateCookie(w, r, rp) if err != nil { @@ -411,7 +418,11 @@ func CodeExchangeHandler(callback CodeExchangeCallback, rp RelyingParty) http.Ha rp.ErrorHandler()(w, r, params.Get("error"), params.Get("error_description"), state) return } - codeOpts := make([]CodeExchangeOpt, 0) + codeOpts := make([]CodeExchangeOpt, len(urlParam)) + for i, p := range urlParam { + codeOpts[i] = CodeExchangeOpt(p) + } + if rp.IsPKCE() { codeVerifier, err := rp.CookieHandler().CheckCookie(r, pkceCode) if err != nil { @@ -517,6 +528,37 @@ func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints { } } +// withURLParam sets custom url paramaters. +// This is the generalized, unexported, function used by both +// URLParamOpt and AuthURLOpt. +func withURLParam(key, value string) func() []oauth2.AuthCodeOption { + return func() []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam(key, value), + } + } +} + +// withPrompt sets the `prompt` params in the auth request +// This is the generalized, unexported, function used by both +// URLParamOpt and AuthURLOpt. +func withPrompt(prompt ...string) func() []oauth2.AuthCodeOption { + return withURLParam("prompt", oidc.SpaceDelimitedArray(prompt).Encode()) +} + +type URLParamOpt func() []oauth2.AuthCodeOption + +// WithURLParam allows setting custom key-vale pairs +// to an OAuth2 URL. +func WithURLParam(key, value string) URLParamOpt { + return withURLParam(key, value) +} + +// WithPromptURLParam sets the `prompt` parameter in a URL. +func WithPromptURLParam(prompt ...string) URLParamOpt { + return withPrompt(prompt...) +} + type AuthURLOpt func() []oauth2.AuthCodeOption // WithCodeChallenge sets the `code_challenge` params in the auth request @@ -531,11 +573,7 @@ func WithCodeChallenge(codeChallenge string) AuthURLOpt { // WithPrompt sets the `prompt` params in the auth request func WithPrompt(prompt ...string) AuthURLOpt { - return func() []oauth2.AuthCodeOption { - return []oauth2.AuthCodeOption{ - oauth2.SetAuthURLParam("prompt", oidc.SpaceDelimitedArray(prompt).Encode()), - } - } + return withPrompt(prompt...) } type CodeExchangeOpt func() []oauth2.AuthCodeOption From 9291ca9908db696c5a8a4d71875f35f68f83dd45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 17 Feb 2023 14:42:02 +0200 Subject: [PATCH 173/502] rp/mock: update go generate package and type Fixes #281 --- pkg/client/rp/mock/generate.go | 2 +- pkg/client/rp/mock/verifier.mock.go | 168 ++++++++++++++++++++++------ 2 files changed, 133 insertions(+), 37 deletions(-) diff --git a/pkg/client/rp/mock/generate.go b/pkg/client/rp/mock/generate.go index 1e05701..7db81ea 100644 --- a/pkg/client/rp/mock/generate.go +++ b/pkg/client/rp/mock/generate.go @@ -1,3 +1,3 @@ package mock -//go:generate mockgen -package mock -destination ./verifier.mock.go github.com/zitadel/oidc/pkg/rp Verifier +//go:generate mockgen -package mock -destination ./verifier.mock.go github.com/zitadel/oidc/v2/pkg/client/rp IDTokenVerifier diff --git a/pkg/client/rp/mock/verifier.mock.go b/pkg/client/rp/mock/verifier.mock.go index 9d1daa1..eac6a79 100644 --- a/pkg/client/rp/mock/verifier.mock.go +++ b/pkg/client/rp/mock/verifier.mock.go @@ -1,67 +1,163 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/pkg/rp (interfaces: Verifier) +// Source: github.com/zitadel/oidc/v2/pkg/client/rp (interfaces: IDTokenVerifier) // Package mock is a generated GoMock package. package mock import ( - "context" - "reflect" + context "context" + reflect "reflect" + time "time" - "github.com/golang/mock/gomock" - - "github.com/zitadel/oidc/v2/pkg/oidc" + gomock "github.com/golang/mock/gomock" + oidc "github.com/zitadel/oidc/v2/pkg/oidc" ) -// MockVerifier is a mock of Verifier interface -type MockVerifier struct { +// MockIDTokenVerifier is a mock of IDTokenVerifier interface. +type MockIDTokenVerifier struct { ctrl *gomock.Controller - recorder *MockVerifierMockRecorder + recorder *MockIDTokenVerifierMockRecorder } -// MockVerifierMockRecorder is the mock recorder for MockVerifier -type MockVerifierMockRecorder struct { - mock *MockVerifier +// MockIDTokenVerifierMockRecorder is the mock recorder for MockIDTokenVerifier. +type MockIDTokenVerifierMockRecorder struct { + mock *MockIDTokenVerifier } -// NewMockVerifier creates a new mock instance -func NewMockVerifier(ctrl *gomock.Controller) *MockVerifier { - mock := &MockVerifier{ctrl: ctrl} - mock.recorder = &MockVerifierMockRecorder{mock} +// NewMockIDTokenVerifier creates a new mock instance. +func NewMockIDTokenVerifier(ctrl *gomock.Controller) *MockIDTokenVerifier { + mock := &MockIDTokenVerifier{ctrl: ctrl} + mock.recorder = &MockIDTokenVerifierMockRecorder{mock} return mock } -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockVerifier) EXPECT() *MockVerifierMockRecorder { +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIDTokenVerifier) EXPECT() *MockIDTokenVerifierMockRecorder { return m.recorder } -// Verify mocks base method -func (m *MockVerifier) Verify(arg0 context.Context, arg1, arg2 string) (*oidc.IDTokenClaims, error) { +// ACR mocks base method. +func (m *MockIDTokenVerifier) ACR() oidc.ACRVerifier { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Verify", arg0, arg1, arg2) - ret0, _ := ret[0].(*oidc.IDTokenClaims) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret := m.ctrl.Call(m, "ACR") + ret0, _ := ret[0].(oidc.ACRVerifier) + return ret0 } -// Verify indicates an expected call of Verify -func (mr *MockVerifierMockRecorder) Verify(arg0, arg1, arg2 interface{}) *gomock.Call { +// ACR indicates an expected call of ACR. +func (mr *MockIDTokenVerifierMockRecorder) ACR() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockVerifier)(nil).Verify), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ACR", reflect.TypeOf((*MockIDTokenVerifier)(nil).ACR)) } -// VerifyIDToken mocks base method -func (m *MockVerifier) VerifyIDToken(arg0 context.Context, arg1 string) (*oidc.IDTokenClaims, error) { +// ClientID mocks base method. +func (m *MockIDTokenVerifier) ClientID() string { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "VerifyIDToken", arg0, arg1) - ret0, _ := ret[0].(*oidc.IDTokenClaims) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret := m.ctrl.Call(m, "ClientID") + ret0, _ := ret[0].(string) + return ret0 } -// VerifyIDToken indicates an expected call of VerifyIDToken -func (mr *MockVerifierMockRecorder) VerifyIDToken(arg0, arg1 interface{}) *gomock.Call { +// ClientID indicates an expected call of ClientID. +func (mr *MockIDTokenVerifierMockRecorder) ClientID() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyIDToken", reflect.TypeOf((*MockVerifier)(nil).VerifyIDToken), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientID", reflect.TypeOf((*MockIDTokenVerifier)(nil).ClientID)) +} + +// Issuer mocks base method. +func (m *MockIDTokenVerifier) Issuer() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Issuer") + ret0, _ := ret[0].(string) + return ret0 +} + +// Issuer indicates an expected call of Issuer. +func (mr *MockIDTokenVerifierMockRecorder) Issuer() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Issuer", reflect.TypeOf((*MockIDTokenVerifier)(nil).Issuer)) +} + +// KeySet mocks base method. +func (m *MockIDTokenVerifier) KeySet() oidc.KeySet { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "KeySet") + ret0, _ := ret[0].(oidc.KeySet) + return ret0 +} + +// KeySet indicates an expected call of KeySet. +func (mr *MockIDTokenVerifierMockRecorder) KeySet() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeySet", reflect.TypeOf((*MockIDTokenVerifier)(nil).KeySet)) +} + +// MaxAge mocks base method. +func (m *MockIDTokenVerifier) MaxAge() time.Duration { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MaxAge") + ret0, _ := ret[0].(time.Duration) + return ret0 +} + +// MaxAge indicates an expected call of MaxAge. +func (mr *MockIDTokenVerifierMockRecorder) MaxAge() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxAge", reflect.TypeOf((*MockIDTokenVerifier)(nil).MaxAge)) +} + +// MaxAgeIAT mocks base method. +func (m *MockIDTokenVerifier) MaxAgeIAT() time.Duration { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MaxAgeIAT") + ret0, _ := ret[0].(time.Duration) + return ret0 +} + +// MaxAgeIAT indicates an expected call of MaxAgeIAT. +func (mr *MockIDTokenVerifierMockRecorder) MaxAgeIAT() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxAgeIAT", reflect.TypeOf((*MockIDTokenVerifier)(nil).MaxAgeIAT)) +} + +// Nonce mocks base method. +func (m *MockIDTokenVerifier) Nonce(arg0 context.Context) string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Nonce", arg0) + ret0, _ := ret[0].(string) + return ret0 +} + +// Nonce indicates an expected call of Nonce. +func (mr *MockIDTokenVerifierMockRecorder) Nonce(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Nonce", reflect.TypeOf((*MockIDTokenVerifier)(nil).Nonce), arg0) +} + +// Offset mocks base method. +func (m *MockIDTokenVerifier) Offset() time.Duration { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Offset") + ret0, _ := ret[0].(time.Duration) + return ret0 +} + +// Offset indicates an expected call of Offset. +func (mr *MockIDTokenVerifierMockRecorder) Offset() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Offset", reflect.TypeOf((*MockIDTokenVerifier)(nil).Offset)) +} + +// SupportedSignAlgs mocks base method. +func (m *MockIDTokenVerifier) SupportedSignAlgs() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SupportedSignAlgs") + ret0, _ := ret[0].([]string) + return ret0 +} + +// SupportedSignAlgs indicates an expected call of SupportedSignAlgs. +func (mr *MockIDTokenVerifierMockRecorder) SupportedSignAlgs() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportedSignAlgs", reflect.TypeOf((*MockIDTokenVerifier)(nil).SupportedSignAlgs)) } From 8e298791d7e6d0d6fdd475c6e2cb1fd94a9bfff7 Mon Sep 17 00:00:00 2001 From: Emil Bektimirov Date: Sun, 19 Feb 2023 14:57:46 +0100 Subject: [PATCH 174/502] feat: Token Exchange (RFC 8693) (#255) This change implements OAuth2 Token Exchange in OP according to RFC 8693 (and client code) Some implementation details: - OP parses and verifies subject/actor tokens natively if they were issued by OP - Third-party tokens verification is also possible by implementing additional storage interface - Token exchange can issue only OP's native tokens (id_token, access_token and refresh_token) with static issuer --- example/client/app/app.go | 25 ++ example/server/storage/client.go | 4 +- example/server/storage/oidc.go | 3 + example/server/storage/storage.go | 135 +++++++- example/server/storage/user.go | 15 + pkg/client/client.go | 15 + pkg/client/{rp => }/integration_test.go | 160 ++++++--- pkg/client/rs/resource_server.go | 5 + pkg/client/tokenexchange/tokenexchange.go | 127 +++++++ pkg/oidc/token.go | 15 + pkg/oidc/token_request.go | 40 ++- pkg/op/op.go | 3 +- pkg/op/storage.go | 43 +++ pkg/op/token.go | 32 +- pkg/op/token_exchange.go | 396 +++++++++++++++++++++- pkg/op/token_request.go | 2 + 16 files changed, 961 insertions(+), 59 deletions(-) rename pkg/client/{rp => }/integration_test.go (72%) create mode 100644 pkg/client/tokenexchange/tokenexchange.go diff --git a/example/client/app/app.go b/example/client/app/app.go index 3e5f19c..97e8948 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -80,6 +80,31 @@ func main() { // w.Write(data) //} + // you can also try token exchange flow + // + // requestTokenExchange := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) { + // data := make(url.Values) + // data.Set("grant_type", string(oidc.GrantTypeTokenExchange)) + // data.Set("requested_token_type", string(oidc.IDTokenType)) + // data.Set("subject_token", tokens.RefreshToken) + // data.Set("subject_token_type", string(oidc.RefreshTokenType)) + // data.Add("scope", "profile custom_scope:impersonate:id2") + + // client := &http.Client{} + // r2, _ := http.NewRequest(http.MethodPost, issuer+"/oauth/token", strings.NewReader(data.Encode())) + // // r2.Header.Add("Authorization", "Basic "+"d2ViOnNlY3JldA==") + // r2.Header.Add("Content-Type", "application/x-www-form-urlencoded") + // r2.SetBasicAuth("web", "secret") + + // resp, _ := client.Do(r2) + // fmt.Println(resp.Status) + + // b, _ := io.ReadAll(resp.Body) + // resp.Body.Close() + + // w.Write(b) + // } + // register the CodeExchangeHandler at the callbackPath // the CodeExchangeHandler handles the auth response, creates the token request and calls the callback function // with the returned tokens from the token endpoint diff --git a/example/server/storage/client.go b/example/server/storage/client.go index bd6ff3c..5a5b33f 100644 --- a/example/server/storage/client.go +++ b/example/server/storage/client.go @@ -158,7 +158,7 @@ func NativeClient(id string, redirectURIs ...string) *Client { loginURL: defaultLoginURL, responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode}, grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken}, - accessTokenType: 0, + accessTokenType: op.AccessTokenTypeBearer, devMode: false, idTokenUserinfoClaimsAssertion: false, clockSkew: 0, @@ -184,7 +184,7 @@ func WebClient(id, secret string, redirectURIs ...string) *Client { loginURL: defaultLoginURL, responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode}, grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken}, - accessTokenType: 0, + accessTokenType: op.AccessTokenTypeBearer, devMode: false, idTokenUserinfoClaimsAssertion: false, clockSkew: 0, diff --git a/example/server/storage/oidc.go b/example/server/storage/oidc.go index 505ab72..83db739 100644 --- a/example/server/storage/oidc.go +++ b/example/server/storage/oidc.go @@ -16,6 +16,9 @@ const ( // CustomClaim is an example for how to return custom claims with this library CustomClaim = "custom_claim" + + // CustomScopeImpersonatePrefix is an example scope prefix for passing user id to impersonate using token exchage + CustomScopeImpersonatePrefix = "custom_scope:impersonate:" ) type AuthRequest struct { diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index 64bffc8..662132c 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -4,8 +4,10 @@ import ( "context" "crypto/rand" "crypto/rsa" + "errors" "fmt" "math/big" + "strings" "sync" "time" @@ -213,11 +215,14 @@ func (s *Storage) DeleteAuthRequest(ctx context.Context, id string) error { // it will be called for all requests able to return an access token (Authorization Code Flow, Implicit Flow, JWT Profile, ...) func (s *Storage) CreateAccessToken(ctx context.Context, request op.TokenRequest) (string, time.Time, error) { var applicationID string - // if authenticated for an app (auth code / implicit flow) we must save the client_id to the token - authReq, ok := request.(*AuthRequest) - if ok { - applicationID = authReq.ApplicationID + switch req := request.(type) { + case *AuthRequest: + // if authenticated for an app (auth code / implicit flow) we must save the client_id to the token + applicationID = req.ApplicationID + case op.TokenExchangeRequest: + applicationID = req.GetClientID() } + token, err := s.accessToken(applicationID, "", request.GetSubject(), request.GetAudience(), request.GetScopes()) if err != nil { return "", time.Time{}, err @@ -228,6 +233,11 @@ func (s *Storage) CreateAccessToken(ctx context.Context, request op.TokenRequest // CreateAccessAndRefreshTokens implements the op.Storage interface // it will be called for all requests able to return an access and refresh token (Authorization Code Flow, Refresh Token Request) func (s *Storage) CreateAccessAndRefreshTokens(ctx context.Context, request op.TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) { + // generate tokens via token exchange flow if request is relevant + if teReq, ok := request.(op.TokenExchangeRequest); ok { + return s.exchangeRefreshToken(ctx, teReq) + } + // get the information depending on the request type / implementation applicationID, authTime, amr := getInfoFromRequest(request) @@ -258,6 +268,24 @@ func (s *Storage) CreateAccessAndRefreshTokens(ctx context.Context, request op.T return accessToken.ID, refreshToken, accessToken.Expiration, nil } +func (s *Storage) exchangeRefreshToken(ctx context.Context, request op.TokenExchangeRequest) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) { + applicationID := request.GetClientID() + authTime := request.GetAuthTime() + + refreshTokenID := uuid.NewString() + accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes()) + if err != nil { + return "", "", time.Time{}, err + } + + refreshToken, err := s.createRefreshToken(accessToken, nil, authTime) + if err != nil { + return "", "", time.Time{}, err + } + + return accessToken.ID, refreshToken, accessToken.Expiration, nil +} + // TokenRequestByRefreshToken implements the op.Storage interface // it will be called after parsing and validation of the refresh token request func (s *Storage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (op.RefreshTokenRequest, error) { @@ -444,6 +472,10 @@ func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection o // GetPrivateClaimsFromScopes implements the op.Storage interface // it will be called for the creation of a JWT access token to assert claims for custom scopes func (s *Storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) { + return s.getPrivateClaimsFromScopes(ctx, userID, clientID, scopes) +} + +func (s *Storage) getPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) { for _, scope := range scopes { switch scope { case CustomScope: @@ -580,6 +612,101 @@ func (s *Storage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSetter, return nil } +// ValidateTokenExchangeRequest implements the op.TokenExchangeStorage interface +// it will be called to validate parsed Token Exchange Grant request +func (s *Storage) ValidateTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) error { + if request.GetRequestedTokenType() == "" { + request.SetRequestedTokenType(oidc.RefreshTokenType) + } + + // Just an example, some use cases might need this use case + if request.GetExchangeSubjectTokenType() == oidc.IDTokenType && request.GetRequestedTokenType() == oidc.RefreshTokenType { + return errors.New("exchanging id_token to refresh_token is not supported") + } + + // Check impersonation permissions + if request.GetExchangeActor() == "" && !s.userStore.GetUserByID(request.GetExchangeSubject()).IsAdmin { + return errors.New("user doesn't have impersonation permission") + } + + allowedScopes := make([]string, 0) + for _, scope := range request.GetScopes() { + if scope == oidc.ScopeAddress { + continue + } + + if strings.HasPrefix(scope, CustomScopeImpersonatePrefix) { + subject := strings.TrimPrefix(scope, CustomScopeImpersonatePrefix) + request.SetSubject(subject) + } + + allowedScopes = append(allowedScopes, scope) + } + + request.SetCurrentScopes(allowedScopes) + + return nil +} + +// ValidateTokenExchangeRequest implements the op.TokenExchangeStorage interface +// Common use case is to store request for audit purposes. For this example we skip the storing. +func (s *Storage) CreateTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) error { + return nil +} + +// GetPrivateClaimsFromScopesForTokenExchange implements the op.TokenExchangeStorage interface +// it will be called for the creation of an exchanged JWT access token to assert claims for custom scopes +// plus adding token exchange specific claims related to delegation or impersonation +func (s *Storage) GetPrivateClaimsFromTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]interface{}, err error) { + claims, err = s.getPrivateClaimsFromScopes(ctx, "", request.GetClientID(), request.GetScopes()) + if err != nil { + return nil, err + } + + for k, v := range s.getTokenExchangeClaims(ctx, request) { + claims = appendClaim(claims, k, v) + } + + return claims, nil +} + +// SetUserinfoFromScopesForTokenExchange implements the op.TokenExchangeStorage interface +// it will be called for the creation of an id_token - we are using the same private function as for other flows, +// plus adding token exchange specific claims related to delegation or impersonation +func (s *Storage) SetUserinfoFromTokenExchangeRequest(ctx context.Context, userinfo oidc.UserInfoSetter, request op.TokenExchangeRequest) error { + err := s.setUserinfo(ctx, userinfo, request.GetSubject(), request.GetClientID(), request.GetScopes()) + if err != nil { + return err + } + + for k, v := range s.getTokenExchangeClaims(ctx, request) { + userinfo.AppendClaims(k, v) + } + + return nil +} + +func (s *Storage) getTokenExchangeClaims(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]interface{}) { + for _, scope := range request.GetScopes() { + switch { + case strings.HasPrefix(scope, CustomScopeImpersonatePrefix) && request.GetExchangeActor() == "": + // Set actor subject claim for impersonation flow + claims = appendClaim(claims, "act", map[string]interface{}{ + "sub": request.GetExchangeSubject(), + }) + } + } + + // Set actor subject claim for delegation flow + // if request.GetExchangeActor() != "" { + // claims = appendClaim(claims, "act", map[string]interface{}{ + // "sub": request.GetExchangeActor(), + // }) + // } + + return claims +} + // getInfoFromRequest returns the clientID, authTime and amr depending on the op.TokenRequest type / implementation func getInfoFromRequest(req op.TokenRequest) (clientID string, authTime time.Time, amr []string) { authReq, ok := req.(*AuthRequest) // Code Flow (with scope offline_access) diff --git a/example/server/storage/user.go b/example/server/storage/user.go index 82c06d0..173daef 100644 --- a/example/server/storage/user.go +++ b/example/server/storage/user.go @@ -18,6 +18,7 @@ type User struct { Phone string PhoneVerified bool PreferredLanguage language.Tag + IsAdmin bool } type Service struct { @@ -49,6 +50,20 @@ func NewUserStore(issuer string) UserStore { Phone: "", PhoneVerified: false, PreferredLanguage: language.German, + IsAdmin: true, + }, + "id2": { + ID: "id2", + Username: "test-user2", + Password: "verysecure", + FirstName: "Test", + LastName: "User2", + Email: "test-user2@zitadel.ch", + EmailVerified: true, + Phone: "", + PhoneVerified: false, + PreferredLanguage: language.German, + IsAdmin: false, }, }, } diff --git a/pkg/client/client.go b/pkg/client/client.go index eaa1a80..077baf2 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -90,6 +90,9 @@ func CallEndSessionEndpoint(request interface{}, authFn interface{}, caller EndS return http.ErrUseLastResponse } resp, err := client.Do(req) + if err != nil { + return nil, err + } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 400 { body, err := io.ReadAll(resp.Body) @@ -148,6 +151,18 @@ func CallRevokeEndpoint(request interface{}, authFn interface{}, caller RevokeCa return nil } +func CallTokenExchangeEndpoint(request interface{}, authFn interface{}, caller TokenEndpointCaller) (resp *oidc.TokenExchangeResponse, err error) { + req, err := httphelper.FormRequest(caller.TokenEndpoint(), request, Encoder, authFn) + if err != nil { + return nil, err + } + tokenRes := new(oidc.TokenExchangeResponse) + if err := httphelper.HttpRequest(caller.HttpClient(), req, &tokenRes); err != nil { + return nil, err + } + return tokenRes, nil +} + func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) { privateKey, err := crypto.BytesToPrivateKey(key) if err != nil { diff --git a/pkg/client/rp/integration_test.go b/pkg/client/integration_test.go similarity index 72% rename from pkg/client/rp/integration_test.go rename to pkg/client/integration_test.go index e29ddd3..e89004a 100644 --- a/pkg/client/rp/integration_test.go +++ b/pkg/client/integration_test.go @@ -1,4 +1,4 @@ -package rp_test +package client_test import ( "bytes" @@ -18,9 +18,12 @@ import ( "github.com/jeremija/gosubmit" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/zitadel/oidc/v2/example/server/exampleop" "github.com/zitadel/oidc/v2/example/server/storage" "github.com/zitadel/oidc/v2/pkg/client/rp" + "github.com/zitadel/oidc/v2/pkg/client/rs" + "github.com/zitadel/oidc/v2/pkg/client/tokenexchange" httphelper "github.com/zitadel/oidc/v2/pkg/http" "github.com/zitadel/oidc/v2/pkg/oidc" ) @@ -36,12 +39,120 @@ func TestRelyingPartySession(t *testing.T) { t.Logf("auth server at %s", opServer.URL) dh.Handler = exampleop.SetupServer(ctx, opServer.URL, exampleStorage) - localURL, err := url.Parse(targetURL + "/login?requestID=1234") - require.NoError(t, err, "local url") + seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano())) + clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25) + + t.Log("------- run authorization code flow ------") + provider, _, refreshToken, idToken := RunAuthorizationCodeFlow(t, opServer, clientID, "secret") + + t.Log("------- refresh tokens ------") + + newTokens, err := rp.RefreshAccessToken(provider, refreshToken, "", "") + require.NoError(t, err, "refresh token") + assert.NotNil(t, newTokens, "access token") + t.Logf("new access token %s", newTokens.AccessToken) + t.Logf("new refresh token %s", newTokens.RefreshToken) + t.Logf("new token type %s", newTokens.TokenType) + t.Logf("new expiry %s", newTokens.Expiry.Format(time.RFC3339)) + require.NotEmpty(t, newTokens.AccessToken, "new accessToken") + + t.Log("------ end session (logout) ------") + + newLoc, err := rp.EndSession(provider, idToken, "", "") + require.NoError(t, err, "logout") + if newLoc != nil { + t.Logf("redirect to %s", newLoc) + } else { + t.Logf("no redirect") + } + + t.Log("------ attempt refresh again (should fail) ------") + t.Log("trying original refresh token", refreshToken) + _, err = rp.RefreshAccessToken(provider, refreshToken, "", "") + assert.Errorf(t, err, "refresh with original") + if newTokens.RefreshToken != "" { + t.Log("trying replacement refresh token", newTokens.RefreshToken) + _, err = rp.RefreshAccessToken(provider, newTokens.RefreshToken, "", "") + assert.Errorf(t, err, "refresh with replacement") + } +} + +func TestResourceServerTokenExchange(t *testing.T) { + t.Log("------- start example OP ------") + ctx := context.Background() + targetURL := "http://local-site" + exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL)) + var dh deferredHandler + opServer := httptest.NewServer(&dh) + defer opServer.Close() + t.Logf("auth server at %s", opServer.URL) + dh.Handler = exampleop.SetupServer(ctx, opServer.URL, exampleStorage) seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano())) clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25) - client := storage.WebClient(clientID, "secret", targetURL) + clientSecret := "secret" + + t.Log("------- run authorization code flow ------") + provider, _, refreshToken, idToken := RunAuthorizationCodeFlow(t, opServer, clientID, clientSecret) + + resourceServer, err := rs.NewResourceServerClientCredentials(opServer.URL, clientID, clientSecret) + require.NoError(t, err, "new resource server") + + t.Log("------- exchage refresh tokens (impersonation) ------") + + tokenExchangeResponse, err := tokenexchange.ExchangeToken( + resourceServer, + refreshToken, + oidc.RefreshTokenType, + "", + "", + []string{}, + []string{}, + []string{"profile", "custom_scope:impersonate:id2"}, + oidc.RefreshTokenType, + ) + require.NoError(t, err, "refresh token") + require.NotNil(t, tokenExchangeResponse, "token exchange response") + assert.Equal(t, tokenExchangeResponse.IssuedTokenType, oidc.RefreshTokenType) + assert.NotEmpty(t, tokenExchangeResponse.AccessToken, "access token") + assert.NotEmpty(t, tokenExchangeResponse.RefreshToken, "refresh token") + assert.Equal(t, []string(tokenExchangeResponse.Scopes), []string{"profile", "custom_scope:impersonate:id2"}) + + t.Log("------ end session (logout) ------") + + newLoc, err := rp.EndSession(provider, idToken, "", "") + require.NoError(t, err, "logout") + if newLoc != nil { + t.Logf("redirect to %s", newLoc) + } else { + t.Logf("no redirect") + } + + t.Log("------- attempt exchage again (should fail) ------") + + tokenExchangeResponse, err = tokenexchange.ExchangeToken( + resourceServer, + refreshToken, + oidc.RefreshTokenType, + "", + "", + []string{}, + []string{}, + []string{"profile", "custom_scope:impersonate:id2"}, + oidc.RefreshTokenType, + ) + require.Error(t, err, "refresh token") + assert.Contains(t, err.Error(), "subject_token is invalid") + require.Nil(t, tokenExchangeResponse, "token exchange response") + +} + +func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID, clientSecret string) (provider rp.RelyingParty, accessToken, refreshToken, idToken string) { + targetURL := "http://local-site" + localURL, err := url.Parse(targetURL + "/login?requestID=1234") + require.NoError(t, err, "local url") + + client := storage.WebClient(clientID, clientSecret, targetURL) storage.RegisterClients(client) jar, err := cookiejar.New(nil) @@ -57,10 +168,10 @@ func TestRelyingPartySession(t *testing.T) { t.Log("------- create RP ------") key := []byte("test1234test1234") cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure()) - provider, err := rp.NewRelyingPartyOIDC( + provider, err = rp.NewRelyingPartyOIDC( opServer.URL, clientID, - "secret", + clientSecret, targetURL, []string{"openid", "email", "profile", "offline_access"}, rp.WithPKCE(cookieHandler), @@ -69,8 +180,10 @@ func TestRelyingPartySession(t *testing.T) { rp.WithSupportedSigningAlgorithms("RS256", "RS384", "RS512", "ES256", "ES384", "ES512"), ), ) + require.NoError(t, err, "new rp") t.Log("------- get redirect from local client (rp) to OP ------") + seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano())) state := "state-" + strconv.FormatInt(seed.Int63(), 25) capturedW := httptest.NewRecorder() get := httptest.NewRequest("GET", localURL.String(), nil) @@ -124,7 +237,7 @@ func TestRelyingPartySession(t *testing.T) { t.Logf("setting cookie %s", cookie) } - var accessToken, refreshToken, idToken, email string + var email string redirect := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) { require.NotNil(t, tokens, "tokens") require.NotNil(t, info, "info") @@ -137,7 +250,7 @@ func TestRelyingPartySession(t *testing.T) { refreshToken = tokens.RefreshToken idToken = tokens.IDToken email = info.GetEmail() - http.Redirect(w, r, targetURL, 302) + http.Redirect(w, r, targetURL, http.StatusFound) } rp.CodeExchangeHandler(rp.UserinfoCallback(redirect), provider)(capturedW, get) @@ -162,36 +275,7 @@ func TestRelyingPartySession(t *testing.T) { assert.NotEmpty(t, accessToken, "access token") assert.NotEmpty(t, email, "email") - t.Log("------- refresh tokens ------") - - newTokens, err := rp.RefreshAccessToken(provider, refreshToken, "", "") - require.NoError(t, err, "refresh token") - assert.NotNil(t, newTokens, "access token") - t.Logf("new access token %s", newTokens.AccessToken) - t.Logf("new refresh token %s", newTokens.RefreshToken) - t.Logf("new token type %s", newTokens.TokenType) - t.Logf("new expiry %s", newTokens.Expiry.Format(time.RFC3339)) - require.NotEmpty(t, newTokens.AccessToken, "new accessToken") - - t.Log("------ end session (logout) ------") - - newLoc, err := rp.EndSession(provider, idToken, "", "") - require.NoError(t, err, "logout") - if newLoc != nil { - t.Logf("redirect to %s", newLoc) - } else { - t.Logf("no redirect") - } - - t.Log("------ attempt refresh again (should fail) ------") - t.Log("trying original refresh token", refreshToken) - _, err = rp.RefreshAccessToken(provider, refreshToken, "", "") - assert.Errorf(t, err, "refresh with original") - if newTokens.RefreshToken != "" { - t.Log("trying replacement refresh token", newTokens.RefreshToken) - _, err = rp.RefreshAccessToken(provider, newTokens.RefreshToken, "", "") - assert.Errorf(t, err, "refresh with replacement") - } + return provider, accessToken, refreshToken, idToken } type deferredHandler struct { diff --git a/pkg/client/rs/resource_server.go b/pkg/client/rs/resource_server.go index 1d9860f..95a0121 100644 --- a/pkg/client/rs/resource_server.go +++ b/pkg/client/rs/resource_server.go @@ -13,6 +13,7 @@ import ( type ResourceServer interface { IntrospectionURL() string + TokenEndpoint() string HttpClient() *http.Client AuthFn() (interface{}, error) } @@ -29,6 +30,10 @@ func (r *resourceServer) IntrospectionURL() string { return r.introspectURL } +func (r *resourceServer) TokenEndpoint() string { + return r.tokenURL +} + func (r *resourceServer) HttpClient() *http.Client { return r.httpClient } diff --git a/pkg/client/tokenexchange/tokenexchange.go b/pkg/client/tokenexchange/tokenexchange.go new file mode 100644 index 0000000..1375f68 --- /dev/null +++ b/pkg/client/tokenexchange/tokenexchange.go @@ -0,0 +1,127 @@ +package tokenexchange + +import ( + "errors" + "net/http" + + "github.com/zitadel/oidc/v2/pkg/client" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" +) + +type TokenExchanger interface { + TokenEndpoint() string + HttpClient() *http.Client + AuthFn() (interface{}, error) +} + +type OAuthTokenExchange struct { + httpClient *http.Client + tokenEndpoint string + authFn func() (interface{}, error) +} + +func NewTokenExchanger(issuer string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) { + return newOAuthTokenExchange(issuer, nil, options...) +} + +func NewTokenExchangerClientCredentials(issuer, clientID, clientSecret string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) { + authorizer := func() (interface{}, error) { + return httphelper.AuthorizeBasic(clientID, clientSecret), nil + } + return newOAuthTokenExchange(issuer, authorizer, options...) +} + +func newOAuthTokenExchange(issuer string, authorizer func() (interface{}, error), options ...func(source *OAuthTokenExchange)) (*OAuthTokenExchange, error) { + te := &OAuthTokenExchange{ + httpClient: httphelper.DefaultHTTPClient, + } + for _, opt := range options { + opt(te) + } + + if te.tokenEndpoint == "" { + config, err := client.Discover(issuer, te.httpClient) + if err != nil { + return nil, err + } + + te.tokenEndpoint = config.TokenEndpoint + } + + if te.tokenEndpoint == "" { + return nil, errors.New("tokenURL is empty: please provide with either `WithStaticTokenEndpoint` or a discovery url") + } + + te.authFn = authorizer + + return te, nil +} + +func WithHTTPClient(client *http.Client) func(*OAuthTokenExchange) { + return func(source *OAuthTokenExchange) { + source.httpClient = client + } +} + +func WithStaticTokenEndpoint(issuer, tokenEndpoint string) func(*OAuthTokenExchange) { + return func(source *OAuthTokenExchange) { + source.tokenEndpoint = tokenEndpoint + } +} + +func (te *OAuthTokenExchange) TokenEndpoint() string { + return te.tokenEndpoint +} + +func (te *OAuthTokenExchange) HttpClient() *http.Client { + return te.httpClient +} + +func (te *OAuthTokenExchange) AuthFn() (interface{}, error) { + if te.authFn != nil { + return te.authFn() + } + + return nil, nil +} + +// ExchangeToken sends a token exchange request (rfc 8693) to te's token endpoint. +// SubjectToken and SubjectTokenType are required parameters. +func ExchangeToken( + te TokenExchanger, + SubjectToken string, + SubjectTokenType oidc.TokenType, + ActorToken string, + ActorTokenType oidc.TokenType, + Resource []string, + Audience []string, + Scopes []string, + RequestedTokenType oidc.TokenType, +) (*oidc.TokenExchangeResponse, error) { + if SubjectToken == "" { + return nil, errors.New("empty subject_token") + } + if SubjectTokenType == "" { + return nil, errors.New("empty subject_token_type") + } + + authFn, err := te.AuthFn() + if err != nil { + return nil, err + } + + request := oidc.TokenExchangeRequest{ + GrantType: oidc.GrantTypeTokenExchange, + SubjectToken: SubjectToken, + SubjectTokenType: SubjectTokenType, + ActorToken: ActorToken, + ActorTokenType: ActorTokenType, + Resource: Resource, + Audience: Audience, + Scopes: Scopes, + RequestedTokenType: RequestedTokenType, + } + + return client.CallTokenExchangeEndpoint(request, authFn, te) +} diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index 198049d..b538465 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -31,6 +31,7 @@ type AccessTokenClaims interface { GetSubject() string GetTokenID() string SetPrivateClaims(map[string]interface{}) + GetClaims() map[string]interface{} } type IDTokenClaims interface { @@ -151,6 +152,11 @@ func (a *accessTokenClaims) SetPrivateClaims(claims map[string]interface{}) { a.claims = claims } +// GetClaims implements the AccessTokenClaims interface +func (a *accessTokenClaims) GetClaims() map[string]interface{} { + return a.claims +} + func (a *accessTokenClaims) MarshalJSON() ([]byte, error) { type Alias accessTokenClaims s := &struct { @@ -612,3 +618,12 @@ func GenerateJWTProfileToken(assertion JWTProfileAssertionClaims) (string, error } return signedAssertion.CompactSerialize() } + +type TokenExchangeResponse struct { + AccessToken string `json:"access_token"` // Can be access token or ID token + IssuedTokenType TokenType `json:"issued_token_type"` + TokenType string `json:"token_type"` + ExpiresIn uint64 `json:"expires_in,omitempty"` + Scopes SpaceDelimitedArray `json:"scope,omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` +} diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index 2b56535..6d8f186 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -40,6 +40,29 @@ var AllGrantTypes = []GrantType{ type GrantType string +const ( + AccessTokenType TokenType = "urn:ietf:params:oauth:token-type:access_token" + RefreshTokenType TokenType = "urn:ietf:params:oauth:token-type:refresh_token" + IDTokenType TokenType = "urn:ietf:params:oauth:token-type:id_token" + JWTTokenType TokenType = "urn:ietf:params:oauth:token-type:jwt" +) + +var AllTokenTypes = []TokenType{ + AccessTokenType, RefreshTokenType, IDTokenType, JWTTokenType, +} + +type TokenType string + +func (t TokenType) IsSupported() bool { + for _, tt := range AllTokenTypes { + if t == tt { + return true + } + } + + return false +} + type TokenRequest interface { // GrantType GrantType `schema:"grant_type"` GrantType() GrantType @@ -203,14 +226,15 @@ func (j *JWTTokenRequest) GetScopes() []string { } type TokenExchangeRequest struct { - subjectToken string `schema:"subject_token"` - subjectTokenType string `schema:"subject_token_type"` - actorToken string `schema:"actor_token"` - actorTokenType string `schema:"actor_token_type"` - resource []string `schema:"resource"` - audience Audience `schema:"audience"` - Scope SpaceDelimitedArray `schema:"scope"` - requestedTokenType string `schema:"requested_token_type"` + GrantType GrantType `schema:"grant_type"` + SubjectToken string `schema:"subject_token"` + SubjectTokenType TokenType `schema:"subject_token_type"` + ActorToken string `schema:"actor_token"` + ActorTokenType TokenType `schema:"actor_token_type"` + Resource []string `schema:"resource"` + Audience Audience `schema:"audience"` + Scopes SpaceDelimitedArray `schema:"scope"` + RequestedTokenType TokenType `schema:"requested_token_type"` } type ClientCredentialsRequest struct { diff --git a/pkg/op/op.go b/pkg/op/op.go index acedcb6..699fb45 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -267,7 +267,8 @@ func (o *Provider) GrantTypeRefreshTokenSupported() bool { } func (o *Provider) GrantTypeTokenExchangeSupported() bool { - return false + _, ok := o.storage.(TokenExchangeStorage) + return ok } func (o *Provider) GrantTypeJWTAuthorizationSupported() bool { diff --git a/pkg/op/storage.go b/pkg/op/storage.go index b040b72..1e19c76 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -25,6 +25,8 @@ type AuthStorage interface { // // * *oidc.JWTTokenRequest from a JWT that is the assertion value of a JWT Profile // Grant: https://datatracker.ietf.org/doc/html/rfc7523#section-2.1 + // + // * TokenExchangeRequest as returned by ValidateTokenExchangeRequest CreateAccessToken(context.Context, TokenRequest) (accessTokenID string, expiration time.Time, err error) // The TokenRequest parameter of CreateAccessAndRefreshTokens can be any of: @@ -36,6 +38,8 @@ type AuthStorage interface { // * AuthRequest as by returned by the AuthRequestByID or AuthRequestByCode (above). // Used for the authorization code flow which requested offline_access scope and // registered the refresh_token grant type in advance + // + // * TokenExchangeRequest as returned by ValidateTokenExchangeRequest CreateAccessAndRefreshTokens(ctx context.Context, request TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshTokenID string, expiration time.Time, err error) TokenRequestByRefreshToken(ctx context.Context, refreshTokenID string) (RefreshTokenRequest, error) @@ -57,6 +61,45 @@ type ClientCredentialsStorage interface { ClientCredentialsTokenRequest(ctx context.Context, clientID string, scopes []string) (TokenRequest, error) } +type TokenExchangeStorage interface { + // ValidateTokenExchangeRequest will be called to validate parsed (including tokens) Token Exchange Grant request. + // + // Important validations can include: + // - permissions + // - set requested token type to some default value if it is empty (rfc 8693 allows it) using SetRequestedTokenType method. + // Depending on RequestedTokenType - the following tokens will be issued: + // - RefreshTokenType - both access and refresh tokens + // - AccessTokenType - only access token + // - IDTokenType - only id token + // - validation of subject's token type on possibility to be exchanged to the requested token type (according to your requirements) + // - scopes (and update them using SetCurrentScopes method) + // - set new subject if it differs from exchange subject (impersonation flow) + // + // Request will include subject's and/or actor's token claims if correspinding tokens are access/id_token issued by op + // or third party tokens parsed by TokenExchangeTokensVerifierStorage interface methods. + ValidateTokenExchangeRequest(ctx context.Context, request TokenExchangeRequest) error + + // CreateTokenExchangeRequest will be called after parsing and validating token exchange request. + // Stored request is not accessed later by op - so it is up to implementer to decide + // should this method actually store the request or not (common use case - store for it for audit purposes) + CreateTokenExchangeRequest(ctx context.Context, request TokenExchangeRequest) error + + // GetPrivateClaimsFromTokenExchangeRequest will be called during access token creation. + // Claims evaluation can be based on all validated request data available, including: scopes, resource, audience, etc. + GetPrivateClaimsFromTokenExchangeRequest(ctx context.Context, request TokenExchangeRequest) (claims map[string]interface{}, err error) + + // SetUserinfoFromTokenExchangeRequest will be called during id token creation. + // Claims evaluation can be based on all validated request data available, including: scopes, resource, audience, etc. + SetUserinfoFromTokenExchangeRequest(ctx context.Context, userinfo oidc.UserInfoSetter, request TokenExchangeRequest) error +} + +// TokenExchangeTokensVerifierStorage is an optional interface used in token exchange process to verify tokens +// issued by third-party applications. If interface is not implemented - only tokens issued by op will be exchanged. +type TokenExchangeTokensVerifierStorage interface { + VerifyExchangeSubjectToken(ctx context.Context, token string, tokenType oidc.TokenType) (tokenIDOrToken string, subject string, tokenClaims map[string]interface{}, err error) + VerifyExchangeActorToken(ctx context.Context, token string, tokenType oidc.TokenType) (tokenIDOrToken string, actor string, tokenClaims map[string]interface{}, err error) +} + // CanRefreshTokenInfo is an optional additional interface that Storage can support. // Supporting CanRefreshTokenInfo is required to be able to (revoke) a refresh token that // is neither an encrypted string of : nor a JWT. diff --git a/pkg/op/token.go b/pkg/op/token.go index 4d3e620..3a35062 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -74,6 +74,8 @@ func needsRefreshToken(tokenRequest TokenRequest, client AccessTokenClient) bool switch req := tokenRequest.(type) { case AuthRequest: return strings.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && req.GetResponseType() == oidc.ResponseTypeCode && ValidateGrantType(client, oidc.GrantTypeRefreshToken) + case TokenExchangeRequest: + return req.GetRequestedTokenType() == oidc.RefreshTokenType case RefreshTokenRequest: return true default: @@ -107,7 +109,23 @@ func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, ex claims := oidc.NewAccessTokenClaims(issuer, tokenRequest.GetSubject(), tokenRequest.GetAudience(), exp, id, client.GetID(), client.ClockSkew()) if client != nil { restrictedScopes := client.RestrictAdditionalAccessTokenScopes()(tokenRequest.GetScopes()) - privateClaims, err := storage.GetPrivateClaimsFromScopes(ctx, tokenRequest.GetSubject(), client.GetID(), removeUserinfoScopes(restrictedScopes)) + + var ( + privateClaims map[string]interface{} + err error + ) + + tokenExchangeRequest, okReq := tokenRequest.(TokenExchangeRequest) + teStorage, okStorage := storage.(TokenExchangeStorage) + if okReq && okStorage { + privateClaims, err = teStorage.GetPrivateClaimsFromTokenExchangeRequest( + ctx, + tokenExchangeRequest, + ) + } else { + privateClaims, err = storage.GetPrivateClaimsFromScopes(ctx, tokenRequest.GetSubject(), client.GetID(), removeUserinfoScopes(restrictedScopes)) + } + if err != nil { return "", err } @@ -156,7 +174,17 @@ func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, v scopes = removeUserinfoScopes(scopes) } } - if len(scopes) > 0 { + + tokenExchangeRequest, okReq := request.(TokenExchangeRequest) + teStorage, okStorage := storage.(TokenExchangeStorage) + if okReq && okStorage { + userInfo := oidc.NewUserInfo() + err := teStorage.SetUserinfoFromTokenExchangeRequest(ctx, userInfo, tokenExchangeRequest) + if err != nil { + return "", err + } + claims.SetUserinfo(userInfo) + } else if len(scopes) > 0 { userInfo := oidc.NewUserInfo() err := storage.SetUserinfoFromScopes(ctx, userInfo, request.GetSubject(), request.GetClientID(), scopes) if err != nil { diff --git a/pkg/op/token_exchange.go b/pkg/op/token_exchange.go index 7bb6e42..6b918b1 100644 --- a/pkg/op/token_exchange.go +++ b/pkg/op/token_exchange.go @@ -1,11 +1,399 @@ package op import ( - "errors" + "context" "net/http" + "net/url" + "strings" + "time" + + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) -// TokenExchange will handle the OAuth 2.0 token exchange grant ("urn:ietf:params:oauth:grant-type:token-exchange") -func TokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { - RequestError(w, r, errors.New("unimplemented")) +type TokenExchangeRequest interface { + GetAMR() []string + GetAudience() []string + GetResourses() []string + GetAuthTime() time.Time + GetClientID() string + GetScopes() []string + GetSubject() string + GetRequestedTokenType() oidc.TokenType + + GetExchangeSubject() string + GetExchangeSubjectTokenType() oidc.TokenType + GetExchangeSubjectTokenIDOrToken() string + GetExchangeSubjectTokenClaims() map[string]interface{} + + GetExchangeActor() string + GetExchangeActorTokenType() oidc.TokenType + GetExchangeActorTokenIDOrToken() string + GetExchangeActorTokenClaims() map[string]interface{} + + SetCurrentScopes(scopes []string) + SetRequestedTokenType(tt oidc.TokenType) + SetSubject(subject string) +} + +type tokenExchangeRequest struct { + exchangeSubjectTokenIDOrToken string + exchangeSubjectTokenType oidc.TokenType + exchangeSubject string + exchangeSubjectTokenClaims map[string]interface{} + + exchangeActorTokenIDOrToken string + exchangeActorTokenType oidc.TokenType + exchangeActor string + exchangeActorTokenClaims map[string]interface{} + + resource []string + audience oidc.Audience + scopes oidc.SpaceDelimitedArray + requestedTokenType oidc.TokenType + clientID string + authTime time.Time + subject string +} + +func (r *tokenExchangeRequest) GetAMR() []string { + return []string{} +} + +func (r *tokenExchangeRequest) GetAudience() []string { + return r.audience +} + +func (r *tokenExchangeRequest) GetResourses() []string { + return r.resource +} + +func (r *tokenExchangeRequest) GetAuthTime() time.Time { + return r.authTime +} + +func (r *tokenExchangeRequest) GetClientID() string { + return r.clientID +} + +func (r *tokenExchangeRequest) GetScopes() []string { + return r.scopes +} + +func (r *tokenExchangeRequest) GetRequestedTokenType() oidc.TokenType { + return r.requestedTokenType +} + +func (r *tokenExchangeRequest) GetExchangeSubject() string { + return r.exchangeSubject +} + +func (r *tokenExchangeRequest) GetExchangeSubjectTokenType() oidc.TokenType { + return r.exchangeSubjectTokenType +} + +func (r *tokenExchangeRequest) GetExchangeSubjectTokenIDOrToken() string { + return r.exchangeSubjectTokenIDOrToken +} + +func (r *tokenExchangeRequest) GetExchangeSubjectTokenClaims() map[string]interface{} { + return r.exchangeSubjectTokenClaims +} + +func (r *tokenExchangeRequest) GetExchangeActor() string { + return r.exchangeActor +} + +func (r *tokenExchangeRequest) GetExchangeActorTokenType() oidc.TokenType { + return r.exchangeActorTokenType +} + +func (r *tokenExchangeRequest) GetExchangeActorTokenIDOrToken() string { + return r.exchangeActorTokenIDOrToken +} + +func (r *tokenExchangeRequest) GetExchangeActorTokenClaims() map[string]interface{} { + return r.exchangeActorTokenClaims +} + +func (r *tokenExchangeRequest) GetSubject() string { + return r.subject +} + +func (r *tokenExchangeRequest) SetCurrentScopes(scopes []string) { + r.scopes = scopes +} + +func (r *tokenExchangeRequest) SetRequestedTokenType(tt oidc.TokenType) { + r.requestedTokenType = tt +} + +func (r *tokenExchangeRequest) SetSubject(subject string) { + r.subject = subject +} + +// TokenExchange handles the OAuth 2.0 token exchange grant ("urn:ietf:params:oauth:grant-type:token-exchange") +func TokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { + tokenExchangeReq, clientID, clientSecret, err := ParseTokenExchangeRequest(r, exchanger.Decoder()) + if err != nil { + RequestError(w, r, err) + } + + tokenExchangeRequest, client, err := ValidateTokenExchangeRequest(r.Context(), tokenExchangeReq, clientID, clientSecret, exchanger) + if err != nil { + RequestError(w, r, err) + return + } + resp, err := CreateTokenExchangeResponse(r.Context(), tokenExchangeRequest, client, exchanger) + if err != nil { + RequestError(w, r, err) + return + } + httphelper.MarshalJSON(w, resp) +} + +// ParseTokenExchangeRequest parses the http request into oidc.TokenExchangeRequest +func ParseTokenExchangeRequest(r *http.Request, decoder httphelper.Decoder) (_ *oidc.TokenExchangeRequest, clientID, clientSecret string, err error) { + err = r.ParseForm() + if err != nil { + return nil, "", "", oidc.ErrInvalidRequest().WithDescription("error parsing form").WithParent(err) + } + + request := new(oidc.TokenExchangeRequest) + err = decoder.Decode(request, r.Form) + if err != nil { + return nil, "", "", oidc.ErrInvalidRequest().WithDescription("error decoding form").WithParent(err) + } + + var ok bool + if clientID, clientSecret, ok = r.BasicAuth(); ok { + clientID, err = url.QueryUnescape(clientID) + if err != nil { + return nil, "", "", oidc.ErrInvalidClient().WithDescription("invalid basic auth header").WithParent(err) + } + + clientSecret, err = url.QueryUnescape(clientSecret) + if err != nil { + return nil, "", "", oidc.ErrInvalidClient().WithDescription("invalid basic auth header").WithParent(err) + } + } + + return request, clientID, clientSecret, nil +} + +// ValidateTokenExchangeRequest validates the token exchange request parameters including authorization check of the client, +// subject_token and actor_token +func ValidateTokenExchangeRequest( + ctx context.Context, + oidcTokenExchangeRequest *oidc.TokenExchangeRequest, + clientID, clientSecret string, + exchanger Exchanger, +) (TokenExchangeRequest, Client, error) { + if oidcTokenExchangeRequest.SubjectToken == "" { + return nil, nil, oidc.ErrInvalidRequest().WithDescription("subject_token missing") + } + + if oidcTokenExchangeRequest.SubjectTokenType == "" { + return nil, nil, oidc.ErrInvalidRequest().WithDescription("subject_token_type missing") + } + + storage := exchanger.Storage() + teStorage, ok := storage.(TokenExchangeStorage) + if !ok { + return nil, nil, oidc.ErrUnsupportedGrantType().WithDescription("token_exchange grant not supported") + } + + client, err := AuthorizeTokenExchangeClient(ctx, clientID, clientSecret, exchanger) + if err != nil { + return nil, nil, err + } + + if oidcTokenExchangeRequest.RequestedTokenType != "" && !oidcTokenExchangeRequest.RequestedTokenType.IsSupported() { + return nil, nil, oidc.ErrInvalidRequest().WithDescription("requested_token_type is not supported") + } + + if !oidcTokenExchangeRequest.SubjectTokenType.IsSupported() { + return nil, nil, oidc.ErrInvalidRequest().WithDescription("subject_token_type is not supported") + } + + if oidcTokenExchangeRequest.ActorTokenType != "" && !oidcTokenExchangeRequest.ActorTokenType.IsSupported() { + return nil, nil, oidc.ErrInvalidRequest().WithDescription("actor_token_type is not supported") + } + + exchangeSubjectTokenIDOrToken, exchangeSubject, exchangeSubjectTokenClaims, ok := GetTokenIDAndSubjectFromToken(ctx, exchanger, + oidcTokenExchangeRequest.SubjectToken, oidcTokenExchangeRequest.SubjectTokenType, false) + if !ok { + return nil, nil, oidc.ErrInvalidRequest().WithDescription("subject_token is invalid") + } + + var ( + exchangeActorTokenIDOrToken, exchangeActor string + exchangeActorTokenClaims map[string]interface{} + ) + if oidcTokenExchangeRequest.ActorToken != "" { + exchangeActorTokenIDOrToken, exchangeActor, exchangeActorTokenClaims, ok = GetTokenIDAndSubjectFromToken(ctx, exchanger, + oidcTokenExchangeRequest.ActorToken, oidcTokenExchangeRequest.ActorTokenType, true) + if !ok { + return nil, nil, oidc.ErrInvalidRequest().WithDescription("actor_token is invalid") + } + } + + req := &tokenExchangeRequest{ + exchangeSubjectTokenIDOrToken: exchangeSubjectTokenIDOrToken, + exchangeSubjectTokenType: oidcTokenExchangeRequest.SubjectTokenType, + exchangeSubject: exchangeSubject, + exchangeSubjectTokenClaims: exchangeSubjectTokenClaims, + + exchangeActorTokenIDOrToken: exchangeActorTokenIDOrToken, + exchangeActorTokenType: oidcTokenExchangeRequest.ActorTokenType, + exchangeActor: exchangeActor, + exchangeActorTokenClaims: exchangeActorTokenClaims, + + subject: exchangeSubject, + resource: oidcTokenExchangeRequest.Resource, + audience: oidcTokenExchangeRequest.Audience, + scopes: oidcTokenExchangeRequest.Scopes, + requestedTokenType: oidcTokenExchangeRequest.RequestedTokenType, + clientID: client.GetID(), + authTime: time.Now(), + } + + err = teStorage.ValidateTokenExchangeRequest(ctx, req) + if err != nil { + return nil, nil, err + } + + err = teStorage.CreateTokenExchangeRequest(ctx, req) + if err != nil { + return nil, nil, err + } + + return req, client, nil +} + +func GetTokenIDAndSubjectFromToken( + ctx context.Context, + exchanger Exchanger, + token string, + tokenType oidc.TokenType, + isActor bool, +) (tokenIDOrToken, subject string, claims map[string]interface{}, ok bool) { + switch tokenType { + case oidc.AccessTokenType: + var accessTokenClaims oidc.AccessTokenClaims + tokenIDOrToken, subject, accessTokenClaims, ok = getTokenIDAndClaims(ctx, exchanger, token) + claims = accessTokenClaims.GetClaims() + case oidc.RefreshTokenType: + refreshTokenRequest, err := exchanger.Storage().TokenRequestByRefreshToken(ctx, token) + if err != nil { + break + } + + tokenIDOrToken, subject, ok = token, refreshTokenRequest.GetSubject(), true + case oidc.IDTokenType: + idTokenClaims, err := VerifyIDTokenHint(ctx, token, exchanger.IDTokenHintVerifier(ctx)) + if err != nil { + break + } + + tokenIDOrToken, subject, claims, ok = token, idTokenClaims.GetSubject(), idTokenClaims.GetClaims(), true + } + + if !ok { + if verifier, ok := exchanger.Storage().(TokenExchangeTokensVerifierStorage); ok { + var err error + if isActor { + tokenIDOrToken, subject, claims, err = verifier.VerifyExchangeActorToken(ctx, token, tokenType) + } else { + tokenIDOrToken, subject, claims, err = verifier.VerifyExchangeSubjectToken(ctx, token, tokenType) + } + if err != nil { + return "", "", nil, false + } + + return tokenIDOrToken, subject, claims, true + } + + return "", "", nil, false + } + + return tokenIDOrToken, subject, claims, true +} + +// AuthorizeTokenExchangeClient authorizes a client by validating the client_id and client_secret +func AuthorizeTokenExchangeClient(ctx context.Context, clientID, clientSecret string, exchanger Exchanger) (client Client, err error) { + if err := AuthorizeClientIDSecret(ctx, clientID, clientSecret, exchanger.Storage()); err != nil { + return nil, err + } + + client, err = exchanger.Storage().GetClientByClientID(ctx, clientID) + if err != nil { + return nil, oidc.ErrInvalidClient().WithParent(err) + } + + return client, nil +} + +func CreateTokenExchangeResponse( + ctx context.Context, + tokenExchangeRequest TokenExchangeRequest, + client Client, + creator TokenCreator, +) (_ *oidc.TokenExchangeResponse, err error) { + + var ( + token, refreshToken, tokenType string + validity time.Duration + ) + + switch tokenExchangeRequest.GetRequestedTokenType() { + case oidc.AccessTokenType, oidc.RefreshTokenType: + token, refreshToken, validity, err = CreateAccessToken(ctx, tokenExchangeRequest, client.AccessTokenType(), creator, client, "") + if err != nil { + return nil, err + } + + tokenType = oidc.BearerToken + case oidc.IDTokenType: + token, err = CreateIDToken(ctx, IssuerFromContext(ctx), tokenExchangeRequest, client.IDTokenLifetime(), "", "", creator.Storage(), client) + if err != nil { + return nil, err + } + + // not applicable (see https://datatracker.ietf.org/doc/html/rfc8693#section-2-2-1-2-6) + tokenType = "N_A" + default: + // oidc.JWTTokenType and other custom token types are not supported for issuing. + // In the future it can be considered to have custom tokens generation logic injected via op configuration + // or via expanding Storage interface + oidc.ErrInvalidRequest().WithDescription("requested_token_type is invalid") + } + + exp := uint64(validity.Seconds()) + return &oidc.TokenExchangeResponse{ + AccessToken: token, + IssuedTokenType: tokenExchangeRequest.GetRequestedTokenType(), + TokenType: tokenType, + ExpiresIn: exp, + RefreshToken: refreshToken, + Scopes: tokenExchangeRequest.GetScopes(), + }, nil +} + +func getTokenIDAndClaims(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, oidc.AccessTokenClaims, bool) { + tokenIDSubject, err := userinfoProvider.Crypto().Decrypt(accessToken) + if err == nil { + splitToken := strings.Split(tokenIDSubject, ":") + if len(splitToken) != 2 { + return "", "", nil, false + } + + return splitToken[0], splitToken[1], nil, true + } + accessTokenClaims, err := VerifyAccessToken(ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx)) + if err != nil { + return "", "", nil, false + } + + return accessTokenClaims.GetTokenID(), accessTokenClaims.GetSubject(), accessTokenClaims, true } diff --git a/pkg/op/token_request.go b/pkg/op/token_request.go index 190e812..3d65ea0 100644 --- a/pkg/op/token_request.go +++ b/pkg/op/token_request.go @@ -19,6 +19,8 @@ type Exchanger interface { GrantTypeTokenExchangeSupported() bool GrantTypeJWTAuthorizationSupported() bool GrantTypeClientCredentialsSupported() bool + AccessTokenVerifier(context.Context) AccessTokenVerifier + IDTokenHintVerifier(context.Context) IDTokenHintVerifier } func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Request) { From f6d107340ebd86f593f96597fbe0695fda70133c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 24 Feb 2023 10:46:14 +0100 Subject: [PATCH 175/502] make `next` branch the new pre-release branch (#284) --- .releaserc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.releaserc.js b/.releaserc.js index 7b9f1ce..e8eea8e 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -1,7 +1,7 @@ module.exports = { branches: [ {name: "main"}, - {name: "dynamic-issuer", prerelease: true}, + {name: "next", prerelease: true}, ], plugins: [ "@semantic-release/commit-analyzer", From 03f71a67c21ff83a3afc0c884d634f465e522f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 24 Feb 2023 10:30:46 +0100 Subject: [PATCH 176/502] readme: update example commands --- README.md | 2 +- example/server/main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 26a08ec..31287e9 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Check the `/example` folder where example code for different scenarios is locate # oidc discovery http://localhost:9998/.well-known/openid-configuration go run github.com/zitadel/oidc/v2/example/server # start oidc web client (in a new terminal) -CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998 SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/example/client/app +CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v2/example/client/app ``` - open http://localhost:9999/login in your browser diff --git a/example/server/main.go b/example/server/main.go index 327e294..6b40305 100644 --- a/example/server/main.go +++ b/example/server/main.go @@ -15,7 +15,7 @@ func main() { //we will run on :9998 port := "9998" - //which gives us the issuer: //http://localhost:9998/ + //which gives us the issuer: http://localhost:9998/ issuer := fmt.Sprintf("http://localhost:%s/", port) // the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations From 815ced424cabc02cf18bb03d362f9b980bc09d41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Thu, 23 Feb 2023 15:31:43 +0100 Subject: [PATCH 177/502] readme: update zitdal docs link Fixes #286 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d0356ce..1e4c26f 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ For your convenience you can find the relevant standards linked below. - [OAuth 2.0 Token Exchange](https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-19) - [OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens](https://tools.ietf.org/html/draft-ietf-oauth-mtls-17) - [JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants](https://tools.ietf.org/html/rfc7523) -- [OIDC/OAuth Flow in Zitadel (using this library)](https://docs.zitadel.com/docs/guides/integrate/login-users) +- [OIDC/OAuth Flow in Zitadel (using this library)](https://zitadel.com/docs/guides/integrate/login-users) ## Supported Go Versions From 2342f208ef94fe835db2b657307cb5c217ee688c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 1 Mar 2023 09:59:17 +0200 Subject: [PATCH 178/502] implement RFC 8628: Device authorization grant --- example/client/device/device.go | 61 +++ example/server/exampleop/device.go | 191 ++++++++ example/server/exampleop/login.go | 44 +- example/server/exampleop/op.go | 14 +- example/server/exampleop/templates.go | 26 + .../exampleop/templates/confirm_device.html | 25 + .../exampleop/templates/device_login.html | 29 ++ example/server/exampleop/templates/login.html | 29 ++ .../server/exampleop/templates/usercode.html | 21 + example/server/storage/storage.go | 97 ++++ pkg/client/client.go | 93 ++++ pkg/client/rp/device.go | 62 +++ pkg/client/rp/relying_party.go | 30 +- pkg/oidc/device_authorization.go | 29 ++ pkg/oidc/discovery.go | 2 + pkg/oidc/error.go | 34 ++ pkg/oidc/token_request.go | 5 +- pkg/op/client.go | 97 ++++ pkg/op/client_test.go | 253 ++++++++++ pkg/op/config.go | 3 + pkg/op/device.go | 265 ++++++++++ pkg/op/device_test.go | 453 ++++++++++++++++++ pkg/op/discovery.go | 4 + pkg/op/discovery_test.go | 2 + pkg/op/mock/configuration.mock.go | 56 +++ pkg/op/op.go | 49 +- pkg/op/storage.go | 47 ++ pkg/op/token_intospection.go | 38 +- pkg/op/token_request.go | 6 + 29 files changed, 1968 insertions(+), 97 deletions(-) create mode 100644 example/client/device/device.go create mode 100644 example/server/exampleop/device.go create mode 100644 example/server/exampleop/templates.go create mode 100644 example/server/exampleop/templates/confirm_device.html create mode 100644 example/server/exampleop/templates/device_login.html create mode 100644 example/server/exampleop/templates/login.html create mode 100644 example/server/exampleop/templates/usercode.html create mode 100644 pkg/client/rp/device.go create mode 100644 pkg/oidc/device_authorization.go create mode 100644 pkg/op/client_test.go create mode 100644 pkg/op/device.go create mode 100644 pkg/op/device_test.go diff --git a/example/client/device/device.go b/example/client/device/device.go new file mode 100644 index 0000000..284ba37 --- /dev/null +++ b/example/client/device/device.go @@ -0,0 +1,61 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/sirupsen/logrus" + + "github.com/zitadel/oidc/v2/pkg/client/rp" + httphelper "github.com/zitadel/oidc/v2/pkg/http" +) + +var ( + key = []byte("test1234test1234") +) + +func main() { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT) + defer stop() + + clientID := os.Getenv("CLIENT_ID") + clientSecret := os.Getenv("CLIENT_SECRET") + keyPath := os.Getenv("KEY_PATH") + issuer := os.Getenv("ISSUER") + scopes := strings.Split(os.Getenv("SCOPES"), " ") + + cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure()) + + var options []rp.Option + if clientSecret == "" { + options = append(options, rp.WithPKCE(cookieHandler)) + } + if keyPath != "" { + options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath))) + } + + provider, err := rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, "", scopes, options...) + if err != nil { + logrus.Fatalf("error creating provider %s", err.Error()) + } + + logrus.Info("starting device authorization flow") + resp, err := rp.DeviceAuthorization(scopes, provider) + if err != nil { + logrus.Fatal(err) + } + logrus.Info("resp", resp) + fmt.Printf("\nPlease browse to %s and enter code %s\n", resp.VerificationURI, resp.UserCode) + + logrus.Info("start polling") + token, err := rp.DeviceAccessToken(ctx, resp.DeviceCode, time.Duration(resp.Interval)*time.Second, provider) + if err != nil { + logrus.Fatal(err) + } + logrus.Infof("successfully obtained token: %v", token) +} diff --git a/example/server/exampleop/device.go b/example/server/exampleop/device.go new file mode 100644 index 0000000..ae2e8f2 --- /dev/null +++ b/example/server/exampleop/device.go @@ -0,0 +1,191 @@ +package exampleop + +import ( + "errors" + "fmt" + "io" + "net/http" + "net/url" + + "github.com/gorilla/mux" + "github.com/gorilla/securecookie" + "github.com/sirupsen/logrus" + "github.com/zitadel/oidc/v2/pkg/op" +) + +type deviceAuthenticate interface { + CheckUsernamePasswordSimple(username, password string) error + op.DeviceAuthorizationStorage +} + +type deviceLogin struct { + storage deviceAuthenticate + cookie *securecookie.SecureCookie +} + +func registerDeviceAuth(storage deviceAuthenticate, router *mux.Router) { + l := &deviceLogin{ + storage: storage, + cookie: securecookie.New(securecookie.GenerateRandomKey(32), nil), + } + + router.HandleFunc("", l.userCodeHandler) + router.Path("/login").Methods(http.MethodPost).HandlerFunc(l.loginHandler) + router.HandleFunc("/confirm", l.confirmHandler) +} + +func renderUserCode(w io.Writer, err error) { + data := struct { + Error string + }{ + Error: errMsg(err), + } + + if err := templates.ExecuteTemplate(w, "usercode", data); err != nil { + logrus.Error(err) + } +} + +func renderDeviceLogin(w http.ResponseWriter, userCode string, err error) { + data := &struct { + UserCode string + Error string + }{ + UserCode: userCode, + Error: errMsg(err), + } + if err = templates.ExecuteTemplate(w, "device_login", data); err != nil { + logrus.Error(err) + } +} + +func renderConfirmPage(w http.ResponseWriter, username, clientID string, scopes []string) { + data := &struct { + Username string + ClientID string + Scopes []string + }{ + Username: username, + ClientID: clientID, + Scopes: scopes, + } + if err := templates.ExecuteTemplate(w, "confirm_device", data); err != nil { + logrus.Error(err) + } +} + +func (d *deviceLogin) userCodeHandler(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + w.WriteHeader(http.StatusBadRequest) + renderUserCode(w, err) + return + } + userCode := r.Form.Get("user_code") + if userCode == "" { + if prompt, _ := url.QueryUnescape(r.Form.Get("prompt")); prompt != "" { + err = errors.New(prompt) + } + renderUserCode(w, err) + return + } + + renderDeviceLogin(w, userCode, nil) +} + +func redirectBack(w http.ResponseWriter, r *http.Request, prompt string) { + values := make(url.Values) + values.Set("prompt", url.QueryEscape(prompt)) + + url := url.URL{ + Path: "/device", + RawQuery: values.Encode(), + } + http.Redirect(w, r, url.String(), http.StatusSeeOther) +} + +const userCodeCookieName = "user_code" + +type userCodeCookie struct { + UserCode string + UserName string +} + +func (d *deviceLogin) loginHandler(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + redirectBack(w, r, err.Error()) + return + } + + userCode := r.PostForm.Get("user_code") + if userCode == "" { + redirectBack(w, r, "missing user_code in request") + return + } + username := r.PostForm.Get("username") + if username == "" { + redirectBack(w, r, "missing username in request") + return + } + password := r.PostForm.Get("password") + if password == "" { + redirectBack(w, r, "missing password in request") + return + } + + if err := d.storage.CheckUsernamePasswordSimple(username, password); err != nil { + redirectBack(w, r, err.Error()) + return + } + state, err := d.storage.GetDeviceAuthorizationByUserCode(r.Context(), userCode) + if err != nil { + redirectBack(w, r, err.Error()) + return + } + + encoded, err := d.cookie.Encode(userCodeCookieName, userCodeCookie{userCode, username}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + cookie := &http.Cookie{ + Name: userCodeCookieName, + Value: encoded, + Path: "/", + } + http.SetCookie(w, cookie) + renderConfirmPage(w, username, state.ClientID, state.Scopes) +} + +func (d *deviceLogin) confirmHandler(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie(userCodeCookieName) + if err != nil { + redirectBack(w, r, err.Error()) + return + } + data := new(userCodeCookie) + if err = d.cookie.Decode(userCodeCookieName, cookie.Value, &data); err != nil { + redirectBack(w, r, err.Error()) + return + } + if err = r.ParseForm(); err != nil { + redirectBack(w, r, err.Error()) + return + } + + action := r.Form.Get("action") + switch action { + case "allowed": + err = d.storage.CompleteDeviceAuthorization(r.Context(), data.UserCode, data.UserName) + case "denied": + err = d.storage.DenyDeviceAuthorization(r.Context(), data.UserCode) + default: + err = errors.New("action must be one of \"allow\" or \"deny\"") + } + if err != nil { + redirectBack(w, r, err.Error()) + return + } + + fmt.Fprintf(w, "Device authorization %s. You can now return to the device", action) +} diff --git a/example/server/exampleop/login.go b/example/server/exampleop/login.go index 5da86d1..c014c9a 100644 --- a/example/server/exampleop/login.go +++ b/example/server/exampleop/login.go @@ -3,45 +3,11 @@ package exampleop import ( "context" "fmt" - "html/template" "net/http" "github.com/gorilla/mux" ) -const ( - queryAuthRequestID = "authRequestID" -) - -var loginTmpl, _ = template.New("login").Parse(` - - - - - Login - - -
- - - -
- - -
- -
- - -
- -

{{.Error}}

- - -
- - `) - type login struct { authenticate authenticate router *mux.Router @@ -74,23 +40,19 @@ func (l *login) loginHandler(w http.ResponseWriter, r *http.Request) { return } // the oidc package will pass the id of the auth request as query parameter - // we will use this id through the login process and therefore pass it to the login page + // we will use this id through the login process and therefore pass it to the login page renderLogin(w, r.FormValue(queryAuthRequestID), nil) } func renderLogin(w http.ResponseWriter, id string, err error) { - var errMsg string - if err != nil { - errMsg = err.Error() - } data := &struct { ID string Error string }{ ID: id, - Error: errMsg, + Error: errMsg(err), } - err = loginTmpl.Execute(w, data) + err = templates.ExecuteTemplate(w, "login", data) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } diff --git a/example/server/exampleop/op.go b/example/server/exampleop/op.go index d3a450c..b46be7f 100644 --- a/example/server/exampleop/op.go +++ b/example/server/exampleop/op.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "log" "net/http" + "time" "github.com/gorilla/mux" "golang.org/x/text/language" @@ -27,7 +28,8 @@ func init() { type Storage interface { op.Storage - CheckUsernamePassword(username, password, id string) error + authenticate + deviceAuthenticate } // SetupServer creates an OIDC server with Issuer=http://localhost: @@ -62,6 +64,9 @@ func SetupServer(ctx context.Context, issuer string, storage Storage) *mux.Route // so we will direct all calls to /login to the login UI router.PathPrefix("/login/").Handler(http.StripPrefix("/login", l.router)) + router.PathPrefix("/device").Subrouter() + registerDeviceAuth(storage, router.PathPrefix("/device").Subrouter()) + // we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration) // is served on the correct path // @@ -99,6 +104,13 @@ func newOP(ctx context.Context, storage op.Storage, issuer string, key [32]byte) // this example has only static texts (in English), so we'll set the here accordingly SupportedUILocales: []language.Tag{language.English}, + + DeviceAuthorization: op.DeviceAuthorizationConfig{ + Lifetime: 5 * time.Minute, + PollInterval: 5 * time.Second, + UserFormURL: issuer + "device", + UserCode: op.UserCodeBase20, + }, } handler, err := op.NewOpenIDProvider(ctx, issuer, config, storage, //we must explicitly allow the use of the http issuer diff --git a/example/server/exampleop/templates.go b/example/server/exampleop/templates.go new file mode 100644 index 0000000..5b5c966 --- /dev/null +++ b/example/server/exampleop/templates.go @@ -0,0 +1,26 @@ +package exampleop + +import ( + "embed" + "html/template" + + "github.com/sirupsen/logrus" +) + +var ( + //go:embed templates + templateFS embed.FS + templates = template.Must(template.ParseFS(templateFS, "templates/*.html")) +) + +const ( + queryAuthRequestID = "authRequestID" +) + +func errMsg(err error) string { + if err == nil { + return "" + } + logrus.Error(err) + return err.Error() +} diff --git a/example/server/exampleop/templates/confirm_device.html b/example/server/exampleop/templates/confirm_device.html new file mode 100644 index 0000000..a6bcdad --- /dev/null +++ b/example/server/exampleop/templates/confirm_device.html @@ -0,0 +1,25 @@ +{{ define "confirm_device" -}} + + + + + Confirm device authorization + + + +

Welcome back {{.Username}}!

+

+ You are about to grant device {{.ClientID}} access to the following scopes: {{.Scopes}}. +

+ + + + +{{- end }} diff --git a/example/server/exampleop/templates/device_login.html b/example/server/exampleop/templates/device_login.html new file mode 100644 index 0000000..cc5b00b --- /dev/null +++ b/example/server/exampleop/templates/device_login.html @@ -0,0 +1,29 @@ +{{ define "device_login" -}} + + + + + Login + + +
+ + + +
+ + +
+ +
+ + +
+ +

{{.Error}}

+ + +
+ + +{{- end }} diff --git a/example/server/exampleop/templates/login.html b/example/server/exampleop/templates/login.html new file mode 100644 index 0000000..b048211 --- /dev/null +++ b/example/server/exampleop/templates/login.html @@ -0,0 +1,29 @@ +{{ define "login" -}} + + + + + Login + + +
+ + + +
+ + +
+ +
+ + +
+ +

{{.Error}}

+ + +
+ +` +{{- end }} \ No newline at end of file diff --git a/example/server/exampleop/templates/usercode.html b/example/server/exampleop/templates/usercode.html new file mode 100644 index 0000000..fb8fa7f --- /dev/null +++ b/example/server/exampleop/templates/usercode.html @@ -0,0 +1,21 @@ +{{ define "usercode" -}} + + + + + Device authorization + + +
+

Device authorization

+
+ + +
+

{{.Error}}

+ + +
+ + +{{- end }} diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index 662132c..b49ce1b 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -44,6 +44,8 @@ type Storage struct { services map[string]Service refreshTokens map[string]*RefreshToken signingKey signingKey + deviceCodes map[string]deviceAuthorizationEntry + userCodes map[string]string } type signingKey struct { @@ -105,6 +107,8 @@ func NewStorage(userStore UserStore) *Storage { algorithm: jose.RS256, key: key, }, + deviceCodes: make(map[string]deviceAuthorizationEntry), + userCodes: make(map[string]string), } } @@ -135,6 +139,17 @@ func (s *Storage) CheckUsernamePassword(username, password, id string) error { return fmt.Errorf("username or password wrong") } +func (s *Storage) CheckUsernamePasswordSimple(username, password string) error { + s.lock.Lock() + defer s.lock.Unlock() + + user := s.userStore.GetUserByUsername(username) + if user != nil && user.Password == password { + return nil + } + return fmt.Errorf("username or password wrong") +} + // CreateAuthRequest implements the op.Storage interface // it will be called after parsing and validation of the authentication request func (s *Storage) CreateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, userID string) (op.AuthRequest, error) { @@ -735,3 +750,85 @@ func appendClaim(claims map[string]interface{}, claim string, value interface{}) claims[claim] = value return claims } + +type deviceAuthorizationEntry struct { + deviceCode string + userCode string + state *op.DeviceAuthorizationState +} + +func (s *Storage) StoreDeviceAuthorization(ctx context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes []string) error { + s.lock.Lock() + defer s.lock.Unlock() + + if _, ok := s.clients[clientID]; !ok { + return errors.New("client not found") + } + + if _, ok := s.userCodes[userCode]; ok { + return op.ErrDuplicateUserCode + } + + s.deviceCodes[deviceCode] = deviceAuthorizationEntry{ + deviceCode: deviceCode, + userCode: userCode, + state: &op.DeviceAuthorizationState{ + ClientID: clientID, + Scopes: scopes, + Expires: expires, + }, + } + + s.userCodes[userCode] = deviceCode + return nil +} + +func (s *Storage) GetDeviceAuthorizatonState(ctx context.Context, clientID, deviceCode string) (*op.DeviceAuthorizationState, error) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + s.lock.Lock() + defer s.lock.Unlock() + + entry, ok := s.deviceCodes[deviceCode] + if !ok || entry.state.ClientID != clientID { + return nil, errors.New("device code not found for client") // is there a standard not found error in the framework? + } + + return entry.state, nil +} + +func (s *Storage) GetDeviceAuthorizationByUserCode(ctx context.Context, userCode string) (*op.DeviceAuthorizationState, error) { + s.lock.Lock() + defer s.lock.Unlock() + + entry, ok := s.deviceCodes[s.userCodes[userCode]] + if !ok { + return nil, errors.New("user code not found") + } + + return entry.state, nil +} + +func (s *Storage) CompleteDeviceAuthorization(ctx context.Context, userCode, subject string) error { + s.lock.Lock() + defer s.lock.Unlock() + + entry, ok := s.deviceCodes[s.userCodes[userCode]] + if !ok { + return errors.New("user code not found") + } + + entry.state.Subject = subject + entry.state.Done = true + return nil +} + +func (s *Storage) DenyDeviceAuthorization(ctx context.Context, userCode string) error { + s.lock.Lock() + defer s.lock.Unlock() + + s.deviceCodes[s.userCodes[userCode]].state.Denied = true + return nil +} diff --git a/pkg/client/client.go b/pkg/client/client.go index 077baf2..b9ae008 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -1,6 +1,8 @@ package client import ( + "context" + "encoding/json" "errors" "fmt" "io" @@ -186,3 +188,94 @@ func SignedJWTProfileAssertion(clientID string, audience []string, expiration ti IssuedAt: oidc.Time(iat), }, signer) } + +type DeviceAuthorizationCaller interface { + GetDeviceAuthorizationEndpoint() string + HttpClient() *http.Client +} + +func CallDeviceAuthorizationEndpoint(request *oidc.ClientCredentialsRequest, caller DeviceAuthorizationCaller) (*oidc.DeviceAuthorizationResponse, error) { + req, err := httphelper.FormRequest(caller.GetDeviceAuthorizationEndpoint(), request, Encoder, nil) + if err != nil { + return nil, err + } + if request.ClientSecret != "" { + req.SetBasicAuth(request.ClientID, request.ClientSecret) + } + + resp := new(oidc.DeviceAuthorizationResponse) + if err := httphelper.HttpRequest(caller.HttpClient(), req, &resp); err != nil { + return nil, err + } + return resp, nil +} + +type DeviceAccessTokenRequest struct { + *oidc.ClientCredentialsRequest + oidc.DeviceAccessTokenRequest +} + +func CallDeviceAccessTokenEndpoint(ctx context.Context, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) { + req, err := httphelper.FormRequest(caller.TokenEndpoint(), request, Encoder, nil) + if err != nil { + return nil, err + } + if request.ClientSecret != "" { + req.SetBasicAuth(request.ClientID, request.ClientSecret) + } + + httpResp, err := caller.HttpClient().Do(req) + if err != nil { + return nil, err + } + defer httpResp.Body.Close() + + resp := new(struct { + *oidc.AccessTokenResponse + *oidc.Error + }) + if err = json.NewDecoder(httpResp.Body).Decode(resp); err != nil { + return nil, err + } + + if httpResp.StatusCode == http.StatusOK { + return resp.AccessTokenResponse, nil + } + + return nil, resp.Error +} + +func PollDeviceAccessTokenEndpoint(ctx context.Context, interval time.Duration, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) { + for { + timer := time.After(interval) + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-timer: + } + + ctx, cancel := context.WithTimeout(ctx, interval) + defer cancel() + + resp, err := CallDeviceAccessTokenEndpoint(ctx, request, caller) + if err == nil { + return resp, nil + } + if errors.Is(err, context.DeadlineExceeded) { + interval += 5 * time.Second + } + var target *oidc.Error + if !errors.As(err, &target) { + return nil, err + } + switch target.ErrorType { + case oidc.AuthorizationPending: + continue + case oidc.SlowDown: + interval += 5 * time.Second + continue + default: + return nil, err + } + } +} diff --git a/pkg/client/rp/device.go b/pkg/client/rp/device.go new file mode 100644 index 0000000..73b67ca --- /dev/null +++ b/pkg/client/rp/device.go @@ -0,0 +1,62 @@ +package rp + +import ( + "context" + "fmt" + "time" + + "github.com/zitadel/oidc/v2/pkg/client" + "github.com/zitadel/oidc/v2/pkg/oidc" +) + +func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.ClientCredentialsRequest, error) { + confg := rp.OAuthConfig() + req := &oidc.ClientCredentialsRequest{ + GrantType: oidc.GrantTypeDeviceCode, + Scope: scopes, + ClientID: confg.ClientID, + ClientSecret: confg.ClientSecret, + } + + if signer := rp.Signer(); signer != nil { + assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer()}, time.Hour, signer) + if err != nil { + return nil, fmt.Errorf("failed to build assertion: %w", err) + } + req.ClientAssertion = assertion + req.ClientAssertionType = oidc.ClientAssertionTypeJWTAssertion + } + + return req, nil +} + +// DeviceAuthorization starts a new Device Authorization flow as defined +// in RFC 8628, section 3.1 and 3.2: +// https://www.rfc-editor.org/rfc/rfc8628#section-3.1 +func DeviceAuthorization(scopes []string, rp RelyingParty) (*oidc.DeviceAuthorizationResponse, error) { + req, err := newDeviceClientCredentialsRequest(scopes, rp) + if err != nil { + return nil, err + } + + return client.CallDeviceAuthorizationEndpoint(req, rp) +} + +// DeviceAccessToken attempts to obtain tokens from a Device Authorization, +// by means of polling as defined in RFC, section 3.3 and 3.4: +// https://www.rfc-editor.org/rfc/rfc8628#section-3.4 +func DeviceAccessToken(ctx context.Context, deviceCode string, interval time.Duration, rp RelyingParty) (resp *oidc.AccessTokenResponse, err error) { + req := &client.DeviceAccessTokenRequest{ + DeviceAccessTokenRequest: oidc.DeviceAccessTokenRequest{ + GrantType: oidc.GrantTypeDeviceCode, + DeviceCode: deviceCode, + }, + } + + req.ClientCredentialsRequest, err = newDeviceClientCredentialsRequest(nil, rp) + if err != nil { + return nil, err + } + + return client.PollDeviceAccessTokenEndpoint(ctx, interval, req, tokenEndpointCaller{rp}) +} diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index d2e3cf7..96fe219 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -59,6 +59,10 @@ type RelyingParty interface { // UserinfoEndpoint returns the userinfo UserinfoEndpoint() string + // GetDeviceAuthorizationEndpoint returns the enpoint which can + // be used to start a DeviceAuthorization flow. + GetDeviceAuthorizationEndpoint() string + // IDTokenVerifier returns the verifier interface used for oidc id_token verification IDTokenVerifier() IDTokenVerifier // ErrorHandler returns the handler used for callback errors @@ -121,6 +125,10 @@ func (rp *relyingParty) UserinfoEndpoint() string { return rp.endpoints.UserinfoURL } +func (rp *relyingParty) GetDeviceAuthorizationEndpoint() string { + return rp.endpoints.DeviceAuthorizationURL +} + func (rp *relyingParty) GetEndSessionEndpoint() string { return rp.endpoints.EndSessionURL } @@ -495,11 +503,12 @@ type OptionFunc func(RelyingParty) type Endpoints struct { oauth2.Endpoint - IntrospectURL string - UserinfoURL string - JKWsURL string - EndSessionURL string - RevokeURL string + IntrospectURL string + UserinfoURL string + JKWsURL string + EndSessionURL string + RevokeURL string + DeviceAuthorizationURL string } func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints { @@ -509,11 +518,12 @@ func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints { AuthStyle: oauth2.AuthStyleAutoDetect, TokenURL: discoveryConfig.TokenEndpoint, }, - IntrospectURL: discoveryConfig.IntrospectionEndpoint, - UserinfoURL: discoveryConfig.UserinfoEndpoint, - JKWsURL: discoveryConfig.JwksURI, - EndSessionURL: discoveryConfig.EndSessionEndpoint, - RevokeURL: discoveryConfig.RevocationEndpoint, + IntrospectURL: discoveryConfig.IntrospectionEndpoint, + UserinfoURL: discoveryConfig.UserinfoEndpoint, + JKWsURL: discoveryConfig.JwksURI, + EndSessionURL: discoveryConfig.EndSessionEndpoint, + RevokeURL: discoveryConfig.RevocationEndpoint, + DeviceAuthorizationURL: discoveryConfig.DeviceAuthorizationEndpoint, } } diff --git a/pkg/oidc/device_authorization.go b/pkg/oidc/device_authorization.go new file mode 100644 index 0000000..68b8efa --- /dev/null +++ b/pkg/oidc/device_authorization.go @@ -0,0 +1,29 @@ +package oidc + +// DeviceAuthorizationRequest implements +// https://www.rfc-editor.org/rfc/rfc8628#section-3.1, +// 3.1 Device Authorization Request. +type DeviceAuthorizationRequest struct { + Scopes SpaceDelimitedArray `schema:"scope"` + ClientID string `schema:"client_id"` +} + +// DeviceAuthorizationResponse implements +// https://www.rfc-editor.org/rfc/rfc8628#section-3.2 +// 3.2. Device Authorization Response. +type DeviceAuthorizationResponse struct { + DeviceCode string `json:"device_code"` + UserCode string `json:"user_code"` + VerificationURI string `json:"verification_uri"` + VerificationURIComplete string `json:"verification_uri_complete,omitempty"` + ExpiresIn int `json:"expires_in"` + Interval int `json:"interval,omitempty"` +} + +// DeviceAccessTokenRequest implements +// https://www.rfc-editor.org/rfc/rfc8628#section-3.4, +// Device Access Token Request. +type DeviceAccessTokenRequest struct { + GrantType GrantType `json:"grant_type" schema:"grant_type"` + DeviceCode string `json:"device_code" schema:"device_code"` +} diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go index fbc417b..3574101 100644 --- a/pkg/oidc/discovery.go +++ b/pkg/oidc/discovery.go @@ -30,6 +30,8 @@ type DiscoveryConfiguration struct { // EndSessionEndpoint is a URL where the RP can perform a redirect to request that the End-User be logged out at the OP. EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` + DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint,omitempty"` + // CheckSessionIframe is a URL where the OP provides an iframe that support cross-origin communications for session state information with the RP Client. CheckSessionIframe string `json:"check_session_iframe,omitempty"` diff --git a/pkg/oidc/error.go b/pkg/oidc/error.go index 5797a59..79acecd 100644 --- a/pkg/oidc/error.go +++ b/pkg/oidc/error.go @@ -18,6 +18,14 @@ const ( InteractionRequired errorType = "interaction_required" LoginRequired errorType = "login_required" RequestNotSupported errorType = "request_not_supported" + + // Additional error codes as defined in + // https://www.rfc-editor.org/rfc/rfc8628#section-3.5 + // Device Access Token Response + AuthorizationPending errorType = "authorization_pending" + SlowDown errorType = "slow_down" + AccessDenied errorType = "access_denied" + ExpiredToken errorType = "expired_token" ) var ( @@ -77,6 +85,32 @@ var ( ErrorType: RequestNotSupported, } } + + // Device Access Token errors: + ErrAuthorizationPending = func() *Error { + return &Error{ + ErrorType: AuthorizationPending, + Description: "The client SHOULD repeat the access token request to the token endpoint, after interval from device authorization response.", + } + } + ErrSlowDown = func() *Error { + return &Error{ + ErrorType: SlowDown, + Description: "Polling should continue, but the interval MUST be increased by 5 seconds for this and all subsequent requests.", + } + } + ErrAccessDenied = func() *Error { + return &Error{ + ErrorType: AccessDenied, + Description: "The authorization request was denied.", + } + } + ErrExpiredDeviceCode = func() *Error { + return &Error{ + ErrorType: ExpiredToken, + Description: "The \"device_code\" has expired.", + } + } ) type Error struct { diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index 6d8f186..78bd658 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -27,6 +27,9 @@ const ( // GrantTypeImplicit defines the grant type `implicit` used for implicit flows that skip the generation and exchange of an Authorization Code GrantTypeImplicit GrantType = "implicit" + // GrantTypeDeviceCode + GrantTypeDeviceCode GrantType = "urn:ietf:params:oauth:grant-type:device_code" + // ClientAssertionTypeJWTAssertion defines the client_assertion_type `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` // used for the OAuth JWT Profile Client Authentication ClientAssertionTypeJWTAssertion = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" @@ -35,7 +38,7 @@ const ( var AllGrantTypes = []GrantType{ GrantTypeCode, GrantTypeRefreshToken, GrantTypeClientCredentials, GrantTypeBearer, GrantTypeTokenExchange, GrantTypeImplicit, - ClientAssertionTypeJWTAssertion, + GrantTypeDeviceCode, ClientAssertionTypeJWTAssertion, } type GrantType string diff --git a/pkg/op/client.go b/pkg/op/client.go index e8a3347..1f5e1c9 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -1,8 +1,13 @@ package op import ( + "context" + "errors" + "net/http" + "net/url" "time" + httphelper "github.com/zitadel/oidc/v2/pkg/http" "github.com/zitadel/oidc/v2/pkg/oidc" ) @@ -57,3 +62,95 @@ func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseT func IsConfidentialType(c Client) bool { return c.ApplicationType() == ApplicationTypeWeb } + +var ( + ErrInvalidAuthHeader = errors.New("invalid basic auth header") + ErrNoClientCredentials = errors.New("no client credentials provided") + ErrMissingClientID = errors.New("client_id missing from request") +) + +type ClientJWTProfile interface { + JWTProfileVerifier(context.Context) JWTProfileVerifier +} + +func ClientJWTAuth(ctx context.Context, ca oidc.ClientAssertionParams, verifier ClientJWTProfile) (clientID string, err error) { + if ca.ClientAssertion == "" { + return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials) + } + + profile, err := VerifyJWTAssertion(ctx, ca.ClientAssertion, verifier.JWTProfileVerifier(ctx)) + if err != nil { + return "", oidc.ErrUnauthorizedClient().WithParent(err).WithDescription("JWT assertion failed") + } + return profile.Issuer, nil +} + +func ClientBasicAuth(r *http.Request, storage Storage) (clientID string, err error) { + clientID, clientSecret, ok := r.BasicAuth() + if !ok { + return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials) + } + clientID, err = url.QueryUnescape(clientID) + if err != nil { + return "", oidc.ErrInvalidClient().WithParent(ErrInvalidAuthHeader) + } + clientSecret, err = url.QueryUnescape(clientSecret) + if err != nil { + return "", oidc.ErrInvalidClient().WithParent(ErrInvalidAuthHeader) + } + if err := storage.AuthorizeClientIDSecret(r.Context(), clientID, clientSecret); err != nil { + return "", oidc.ErrUnauthorizedClient().WithParent(err) + } + return clientID, nil +} + +type ClientProvider interface { + Decoder() httphelper.Decoder + Storage() Storage +} + +type clientData struct { + ClientID string `schema:"client_id"` + oidc.ClientAssertionParams +} + +// ClientIDFromRequest parses the request form and tries to obtain the client ID +// and reports if it is authenticated, using a JWT or static client secrets over +// http basic auth. +// +// If the Provider implements IntrospectorJWTProfile and "client_assertion" is +// present in the form data, JWT assertion will be verified and the +// client ID is taken from there. +// If any of them is absent, basic auth is attempted. +// In absence of basic auth data, the unauthenticated client id from the form +// data is returned. +// +// If no client id can be obtained by any method, oidc.ErrInvalidClient +// is returned with ErrMissingClientID wrapped in it. +func ClientIDFromRequest(r *http.Request, p ClientProvider) (clientID string, authenticated bool, err error) { + err = r.ParseForm() + if err != nil { + return "", false, oidc.ErrInvalidRequest().WithDescription("cannot parse form").WithParent(err) + } + + data := new(clientData) + if err = p.Decoder().Decode(data, r.PostForm); err != nil { + return "", false, err + } + + JWTProfile, ok := p.(ClientJWTProfile) + if ok { + clientID, err = ClientJWTAuth(r.Context(), data.ClientAssertionParams, JWTProfile) + } + if !ok || errors.Is(err, ErrNoClientCredentials) { + clientID, err = ClientBasicAuth(r, p.Storage()) + } + if err == nil { + return clientID, true, nil + } + + if data.ClientID == "" { + return "", false, oidc.ErrInvalidClient().WithParent(ErrMissingClientID) + } + return data.ClientID, false, nil +} diff --git a/pkg/op/client_test.go b/pkg/op/client_test.go new file mode 100644 index 0000000..1af4157 --- /dev/null +++ b/pkg/op/client_test.go @@ -0,0 +1,253 @@ +package op_test + +import ( + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/golang/mock/gomock" + "github.com/gorilla/schema" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v2/pkg/op/mock" +) + +type testClientJWTProfile struct{} + +func (testClientJWTProfile) JWTProfileVerifier(context.Context) op.JWTProfileVerifier { return nil } + +func TestClientJWTAuth(t *testing.T) { + type args struct { + ctx context.Context + ca oidc.ClientAssertionParams + verifier op.ClientJWTProfile + } + tests := []struct { + name string + args args + wantClientID string + wantErr error + }{ + { + name: "empty assertion", + args: args{ + context.Background(), + oidc.ClientAssertionParams{}, + testClientJWTProfile{}, + }, + wantErr: op.ErrNoClientCredentials, + }, + { + name: "verification error", + args: args{ + context.Background(), + oidc.ClientAssertionParams{ + ClientAssertion: "foo", + }, + testClientJWTProfile{}, + }, + wantErr: oidc.ErrParse, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotClientID, err := op.ClientJWTAuth(tt.args.ctx, tt.args.ca, tt.args.verifier) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantClientID, gotClientID) + }) + } +} + +func TestClientBasicAuth(t *testing.T) { + errWrong := errors.New("wrong secret") + + type args struct { + username string + password string + } + tests := []struct { + name string + args *args + storage op.Storage + wantClientID string + wantErr error + }{ + { + name: "no args", + wantErr: op.ErrNoClientCredentials, + }, + { + name: "username unescape err", + args: &args{ + username: "%", + password: "bar", + }, + wantErr: op.ErrInvalidAuthHeader, + }, + { + name: "password unescape err", + args: &args{ + username: "foo", + password: "%", + }, + wantErr: op.ErrInvalidAuthHeader, + }, + { + name: "auth error", + args: &args{ + username: "foo", + password: "wrong", + }, + storage: func() op.Storage { + s := mock.NewMockStorage(gomock.NewController(t)) + s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "wrong").Return(errWrong) + return s + }(), + wantErr: errWrong, + }, + { + name: "auth error", + args: &args{ + username: "foo", + password: "bar", + }, + storage: func() op.Storage { + s := mock.NewMockStorage(gomock.NewController(t)) + s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "bar").Return(nil) + return s + }(), + wantClientID: "foo", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/foo", nil) + if tt.args != nil { + r.SetBasicAuth(tt.args.username, tt.args.password) + } + + gotClientID, err := op.ClientBasicAuth(r, tt.storage) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantClientID, gotClientID) + }) + } +} + +type errReader struct{} + +func (errReader) Read([]byte) (int, error) { + return 0, io.ErrNoProgress +} + +type testClientProvider struct { + storage op.Storage +} + +func (testClientProvider) Decoder() httphelper.Decoder { + return schema.NewDecoder() +} + +func (p testClientProvider) Storage() op.Storage { + return p.storage +} + +func TestClientIDFromRequest(t *testing.T) { + type args struct { + body io.Reader + p op.ClientProvider + } + type basicAuth struct { + username string + password string + } + tests := []struct { + name string + args args + basicAuth *basicAuth + wantClientID string + wantAuthenticated bool + wantErr bool + }{ + { + name: "parse error", + args: args{ + body: errReader{}, + }, + wantErr: true, + }, + { + name: "unauthenticated", + args: args{ + body: strings.NewReader( + url.Values{ + "client_id": []string{"foo"}, + }.Encode(), + ), + p: testClientProvider{ + storage: mock.NewStorage(t), + }, + }, + wantClientID: "foo", + wantAuthenticated: false, + }, + { + name: "authenticated", + args: args{ + body: strings.NewReader( + url.Values{}.Encode(), + ), + p: testClientProvider{ + storage: func() op.Storage { + s := mock.NewMockStorage(gomock.NewController(t)) + s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "bar").Return(nil) + return s + }(), + }, + }, + basicAuth: &basicAuth{ + username: "foo", + password: "bar", + }, + wantClientID: "foo", + wantAuthenticated: true, + }, + { + name: "missing client id", + args: args{ + body: strings.NewReader( + url.Values{}.Encode(), + ), + p: testClientProvider{ + storage: mock.NewStorage(t), + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest(http.MethodPost, "/foo", tt.args.body) + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + if tt.basicAuth != nil { + r.SetBasicAuth(tt.basicAuth.username, tt.basicAuth.password) + } + + gotClientID, gotAuthenticated, err := op.ClientIDFromRequest(r, tt.args.p) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.wantClientID, gotClientID) + assert.Equal(t, tt.wantAuthenticated, gotAuthenticated) + }) + } +} diff --git a/pkg/op/config.go b/pkg/op/config.go index c40fa2d..c40ed39 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -27,6 +27,7 @@ type Configuration interface { RevocationEndpoint() Endpoint EndSessionEndpoint() Endpoint KeysEndpoint() Endpoint + DeviceAuthorizationEndpoint() Endpoint AuthMethodPostSupported() bool CodeMethodS256Supported() bool @@ -36,6 +37,7 @@ type Configuration interface { GrantTypeTokenExchangeSupported() bool GrantTypeJWTAuthorizationSupported() bool GrantTypeClientCredentialsSupported() bool + GrantTypeDeviceCodeSupported() bool IntrospectionAuthMethodPrivateKeyJWTSupported() bool IntrospectionEndpointSigningAlgorithmsSupported() []string RevocationAuthMethodPrivateKeyJWTSupported() bool @@ -44,6 +46,7 @@ type Configuration interface { RequestObjectSigningAlgorithmsSupported() []string SupportedUILocales() []language.Tag + DeviceAuthorization() DeviceAuthorizationConfig } type IssuerFromRequest func(r *http.Request) string diff --git a/pkg/op/device.go b/pkg/op/device.go new file mode 100644 index 0000000..04c06f2 --- /dev/null +++ b/pkg/op/device.go @@ -0,0 +1,265 @@ +package op + +import ( + "context" + "crypto/rand" + "encoding/base64" + "errors" + "fmt" + "math/big" + "net/http" + "strings" + "time" + + httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" +) + +type DeviceAuthorizationConfig struct { + Lifetime time.Duration + PollInterval time.Duration + UserFormURL string // the URL where the user must go to authorize the device + UserCode UserCodeConfig +} + +type UserCodeConfig struct { + CharSet string + CharAmount int + DashInterval int +} + +const ( + CharSetBase20 = "BCDFGHJKLMNPQRSTVWXZ" + CharSetDigits = "0123456789" +) + +var ( + UserCodeBase20 = UserCodeConfig{ + CharSet: CharSetBase20, + CharAmount: 8, + DashInterval: 4, + } + UserCodeDigits = UserCodeConfig{ + CharSet: CharSetDigits, + CharAmount: 9, + DashInterval: 3, + } +) + +func DeviceAuthorizationHandler(o OpenIDProvider) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if err := DeviceAuthorization(w, r, o); err != nil { + RequestError(w, r, err) + } + } +} + +func DeviceAuthorization(w http.ResponseWriter, r *http.Request, o OpenIDProvider) error { + storage, err := assertDeviceStorage(o.Storage()) + if err != nil { + return err + } + + req, err := ParseDeviceCodeRequest(r, o) + if err != nil { + return err + } + + config := o.DeviceAuthorization() + + deviceCode, err := NewDeviceCode(RecommendedDeviceCodeBytes) + if err != nil { + return err + } + userCode, err := NewUserCode([]rune(config.UserCode.CharSet), config.UserCode.CharAmount, config.UserCode.DashInterval) + if err != nil { + return err + } + + expires := time.Now().Add(config.Lifetime) + err = storage.StoreDeviceAuthorization(r.Context(), req.ClientID, deviceCode, userCode, expires, req.Scopes) + if err != nil { + return err + } + + response := &oidc.DeviceAuthorizationResponse{ + DeviceCode: deviceCode, + UserCode: userCode, + VerificationURI: config.UserFormURL, + ExpiresIn: int(config.Lifetime / time.Second), + Interval: int(config.PollInterval / time.Second), + } + + response.VerificationURIComplete = fmt.Sprintf("%s?user_code=%s", config.UserFormURL, userCode) + + httphelper.MarshalJSON(w, response) + return nil +} + +func ParseDeviceCodeRequest(r *http.Request, o OpenIDProvider) (*oidc.DeviceAuthorizationRequest, error) { + clientID, _, err := ClientIDFromRequest(r, o) + if err != nil { + return nil, err + } + + req := new(oidc.DeviceAuthorizationRequest) + if err := o.Decoder().Decode(req, r.Form); err != nil { + return nil, oidc.ErrInvalidRequest().WithDescription("cannot parse device authentication request").WithParent(err) + } + req.ClientID = clientID + + return req, nil +} + +// 16 bytes gives 128 bit of entropy. +// results in a 22 character base64 encoded string. +const RecommendedDeviceCodeBytes = 16 + +func NewDeviceCode(nBytes int) (string, error) { + bytes := make([]byte, nBytes) + if _, err := rand.Read(bytes); err != nil { + return "", fmt.Errorf("%w getting entropy for device code", err) + } + return base64.RawURLEncoding.EncodeToString(bytes), nil +} + +func NewUserCode(charSet []rune, charAmount, dashInterval int) (string, error) { + var buf strings.Builder + if dashInterval > 0 { + buf.Grow(charAmount + charAmount/dashInterval - 1) + } else { + buf.Grow(charAmount) + } + + max := big.NewInt(int64(len(charSet))) + + for i := 0; i < charAmount; i++ { + if dashInterval != 0 && i != 0 && i%dashInterval == 0 { + buf.WriteByte('-') + } + + bi, err := rand.Int(rand.Reader, max) + if err != nil { + return "", fmt.Errorf("%w getting entropy for user code", err) + } + + buf.WriteRune(charSet[int(bi.Int64())]) + } + + return buf.String(), nil +} + +type deviceAccessTokenRequest struct { + subject string + audience []string + scopes []string +} + +func (r *deviceAccessTokenRequest) GetSubject() string { + return r.subject +} + +func (r *deviceAccessTokenRequest) GetAudience() []string { + return r.audience +} + +func (r *deviceAccessTokenRequest) GetScopes() []string { + return r.scopes +} + +func DeviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { + if err := deviceAccessToken(w, r, exchanger); err != nil { + RequestError(w, r, err) + } +} + +func deviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchanger) error { + // use a limited context timeout shorter as the default + // poll interval of 5 seconds. + ctx, cancel := context.WithTimeout(r.Context(), 4*time.Second) + defer cancel() + r = r.WithContext(ctx) + + clientID, clientAuthenticated, err := ClientIDFromRequest(r, exchanger) + if err != nil { + return err + } + + req, err := ParseDeviceAccessTokenRequest(r, exchanger) + if err != nil { + return err + } + state, err := CheckDeviceAuthorizationState(ctx, clientID, req.DeviceCode, exchanger) + if err != nil { + return err + } + + client, err := exchanger.Storage().GetClientByClientID(ctx, clientID) + if err != nil { + return err + } + if clientAuthenticated != IsConfidentialType(client) { + return oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials). + WithDescription("confidential client requires authentication") + } + + tokenRequest := &deviceAccessTokenRequest{ + subject: state.Subject, + audience: []string{clientID}, + scopes: state.Scopes, + } + resp, err := CreateDeviceTokenResponse(r.Context(), tokenRequest, exchanger, client) + if err != nil { + return err + } + + httphelper.MarshalJSON(w, resp) + return nil +} + +func ParseDeviceAccessTokenRequest(r *http.Request, exchanger Exchanger) (*oidc.DeviceAccessTokenRequest, error) { + req := new(oidc.DeviceAccessTokenRequest) + if err := exchanger.Decoder().Decode(req, r.PostForm); err != nil { + return nil, err + } + return req, nil +} + +func CheckDeviceAuthorizationState(ctx context.Context, clientID, deviceCode string, exchanger Exchanger) (*DeviceAuthorizationState, error) { + storage, err := assertDeviceStorage(exchanger.Storage()) + if err != nil { + return nil, err + } + + state, err := storage.GetDeviceAuthorizatonState(ctx, clientID, deviceCode) + if errors.Is(err, context.DeadlineExceeded) { + return nil, oidc.ErrSlowDown().WithParent(err) + } + if err != nil { + return nil, oidc.ErrAccessDenied().WithParent(err) + } + if state.Denied { + return state, oidc.ErrAccessDenied() + } + if state.Done { + return state, nil + } + if time.Now().After(state.Expires) { + return state, oidc.ErrExpiredDeviceCode() + } + return state, oidc.ErrAuthorizationPending() +} + +func CreateDeviceTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator, client AccessTokenClient) (*oidc.AccessTokenResponse, error) { + accessToken, refreshToken, validity, err := CreateAccessToken(ctx, tokenRequest, AccessTokenTypeBearer, creator, client, "") + if err != nil { + return nil, err + } + + return &oidc.AccessTokenResponse{ + AccessToken: accessToken, + RefreshToken: refreshToken, + TokenType: oidc.BearerToken, + ExpiresIn: uint64(validity.Seconds()), + }, nil +} diff --git a/pkg/op/device_test.go b/pkg/op/device_test.go new file mode 100644 index 0000000..ca68759 --- /dev/null +++ b/pkg/op/device_test.go @@ -0,0 +1,453 @@ +package op_test + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "io" + mr "math/rand" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/oidc/v2/example/server/storage" + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" + "golang.org/x/text/language" +) + +var testProvider op.OpenIDProvider + +const ( + testIssuer = "https://localhost:9998/" + pathLoggedOut = "/logged-out" +) + +func init() { + config := &op.Config{ + CryptoKey: sha256.Sum256([]byte("test")), + DefaultLogoutRedirectURI: pathLoggedOut, + CodeMethodS256: true, + AuthMethodPost: true, + AuthMethodPrivateKeyJWT: true, + GrantTypeRefreshToken: true, + RequestObjectSupported: true, + SupportedUILocales: []language.Tag{language.English}, + DeviceAuthorization: op.DeviceAuthorizationConfig{ + Lifetime: 5 * time.Minute, + PollInterval: 5 * time.Second, + UserFormURL: testIssuer + "device", + UserCode: op.UserCodeBase20, + }, + } + + storage.RegisterClients( + storage.NativeClient("native"), + storage.WebClient("web", "secret"), + storage.WebClient("api", "secret"), + ) + + var err error + testProvider, err = op.NewOpenIDProvider(context.TODO(), testIssuer, config, + storage.NewStorage(storage.NewUserStore(testIssuer)), op.WithAllowInsecure(), + ) + if err != nil { + panic(err) + } +} + +func Test_deviceAuthorizationHandler(t *testing.T) { + req := &oidc.DeviceAuthorizationRequest{ + Scopes: []string{"foo", "bar"}, + ClientID: "web", + } + values := make(url.Values) + testProvider.Encoder().Encode(req, values) + body := strings.NewReader(values.Encode()) + + r := httptest.NewRequest(http.MethodPost, "/", body) + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + w := httptest.NewRecorder() + + runWithRandReader(mr.New(mr.NewSource(1)), func() { + op.DeviceAuthorizationHandler(testProvider)(w, r) + }) + + result := w.Result() + + assert.Less(t, result.StatusCode, 300) + + got, _ := io.ReadAll(result.Body) + assert.JSONEq(t, `{"device_code":"Uv38ByGCZU8WP18PmmIdcg", "expires_in":300, "interval":5, "user_code":"JKRV-FRGK", "verification_uri":"https://localhost:9998/device", "verification_uri_complete":"https://localhost:9998/device?user_code=JKRV-FRGK"}`, string(got)) +} + +func TestParseDeviceCodeRequest(t *testing.T) { + tests := []struct { + name string + req *oidc.DeviceAuthorizationRequest + wantErr bool + }{ + { + name: "empty request", + wantErr: true, + }, + /* decoding a SpaceDelimitedArray is broken + https://github.com/zitadel/oidc/issues/295 + { + name: "success", + req: &oidc.DeviceAuthorizationRequest{ + Scopes: oidc.SpaceDelimitedArray{"foo", "bar"}, + ClientID: "web", + }, + }, + */ + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var body io.Reader + if tt.req != nil { + values := make(url.Values) + testProvider.Encoder().Encode(tt.req, values) + body = strings.NewReader(values.Encode()) + } + + r := httptest.NewRequest(http.MethodPost, "/", body) + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + got, err := op.ParseDeviceCodeRequest(r, testProvider) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.req, got) + }) + } +} + +func runWithRandReader(r io.Reader, f func()) { + originalReader := rand.Reader + rand.Reader = r + defer func() { + rand.Reader = originalReader + }() + + f() +} + +func TestNewDeviceCode(t *testing.T) { + t.Run("reader error", func(t *testing.T) { + runWithRandReader(errReader{}, func() { + _, err := op.NewDeviceCode(16) + require.Error(t, err) + }) + }) + + t.Run("different lengths, rand reader", func(t *testing.T) { + for i := 1; i <= 32; i++ { + got, err := op.NewDeviceCode(i) + require.NoError(t, err) + assert.Len(t, got, base64.RawURLEncoding.EncodedLen(i)) + } + }) + +} + +func TestNewUserCode(t *testing.T) { + type args struct { + charset []rune + charAmount int + dashInterval int + } + tests := []struct { + name string + args args + reader io.Reader + want string + wantErr bool + }{ + { + name: "reader error", + args: args{ + charset: []rune(op.CharSetBase20), + charAmount: 8, + dashInterval: 4, + }, + reader: errReader{}, + wantErr: true, + }, + { + name: "base20", + args: args{ + charset: []rune(op.CharSetBase20), + charAmount: 8, + dashInterval: 4, + }, + reader: mr.New(mr.NewSource(1)), + want: "XKCD-HTTD", + }, + { + name: "digits", + args: args{ + charset: []rune(op.CharSetDigits), + charAmount: 9, + dashInterval: 3, + }, + reader: mr.New(mr.NewSource(1)), + want: "271-256-225", + }, + { + name: "no dashes", + args: args{ + charset: []rune(op.CharSetDigits), + charAmount: 9, + }, + reader: mr.New(mr.NewSource(1)), + want: "271256225", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + runWithRandReader(tt.reader, func() { + got, err := op.NewUserCode(tt.args.charset, tt.args.charAmount, tt.args.dashInterval) + if tt.wantErr { + require.ErrorIs(t, err, io.ErrNoProgress) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.want, got) + }) + + }) + } + + t.Run("crypto/rand", func(t *testing.T) { + const testN = 100000 + + for _, c := range []op.UserCodeConfig{op.UserCodeBase20, op.UserCodeDigits} { + t.Run(c.CharSet, func(t *testing.T) { + results := make(map[string]int) + + for i := 0; i < testN; i++ { + code, err := op.NewUserCode([]rune(c.CharSet), c.CharAmount, c.DashInterval) + require.NoError(t, err) + results[code]++ + } + + t.Log(results) + + var duplicates int + for code, count := range results { + assert.Less(t, count, 3, code) + if count == 2 { + duplicates++ + } + } + + }) + } + }) +} + +func BenchmarkNewUserCode(b *testing.B) { + type args struct { + charset []rune + charAmount int + dashInterval int + } + tests := []struct { + name string + args args + reader io.Reader + }{ + { + name: "math rand, base20", + args: args{ + charset: []rune(op.CharSetBase20), + charAmount: 8, + dashInterval: 4, + }, + reader: mr.New(mr.NewSource(1)), + }, + { + name: "math rand, digits", + args: args{ + charset: []rune(op.CharSetDigits), + charAmount: 9, + dashInterval: 3, + }, + reader: mr.New(mr.NewSource(1)), + }, + { + name: "crypto rand, base20", + args: args{ + charset: []rune(op.CharSetBase20), + charAmount: 8, + dashInterval: 4, + }, + reader: rand.Reader, + }, + { + name: "crypto rand, digits", + args: args{ + charset: []rune(op.CharSetDigits), + charAmount: 9, + dashInterval: 3, + }, + reader: rand.Reader, + }, + } + for _, tt := range tests { + runWithRandReader(tt.reader, func() { + b.Run(tt.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := op.NewUserCode(tt.args.charset, tt.args.charAmount, tt.args.dashInterval) + require.NoError(b, err) + } + }) + + }) + } +} + +func TestDeviceAccessToken(t *testing.T) { + storage := testProvider.Storage().(op.DeviceAuthorizationStorage) + storage.StoreDeviceAuthorization(context.Background(), "native", "qwerty", "yuiop", time.Now().Add(time.Minute), []string{"foo"}) + storage.CompleteDeviceAuthorization(context.Background(), "yuiop", "tim") + + values := make(url.Values) + values.Set("client_id", "native") + values.Set("grant_type", string(oidc.GrantTypeDeviceCode)) + values.Set("device_code", "qwerty") + + r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(values.Encode())) + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + + op.DeviceAccessToken(w, r, testProvider) + + result := w.Result() + got, _ := io.ReadAll(result.Body) + t.Log(string(got)) + assert.Less(t, result.StatusCode, 300) + assert.NotEmpty(t, string(got)) +} + +func TestCheckDeviceAuthorizationState(t *testing.T) { + now := time.Now() + + storage := testProvider.Storage().(op.DeviceAuthorizationStorage) + storage.StoreDeviceAuthorization(context.Background(), "native", "pending", "pending", now.Add(time.Minute), []string{"foo"}) + storage.StoreDeviceAuthorization(context.Background(), "native", "denied", "denied", now.Add(time.Minute), []string{"foo"}) + storage.StoreDeviceAuthorization(context.Background(), "native", "completed", "completed", now.Add(time.Minute), []string{"foo"}) + storage.StoreDeviceAuthorization(context.Background(), "native", "expired", "expired", now.Add(-time.Minute), []string{"foo"}) + + storage.DenyDeviceAuthorization(context.Background(), "denied") + storage.CompleteDeviceAuthorization(context.Background(), "completed", "tim") + + exceededCtx, cancel := context.WithTimeout(context.Background(), -time.Second) + defer cancel() + + type args struct { + ctx context.Context + clientID string + deviceCode string + } + tests := []struct { + name string + args args + want *op.DeviceAuthorizationState + wantErr error + }{ + { + name: "pending", + args: args{ + ctx: context.Background(), + clientID: "native", + deviceCode: "pending", + }, + want: &op.DeviceAuthorizationState{ + ClientID: "native", + Scopes: []string{"foo"}, + Expires: now.Add(time.Minute), + }, + wantErr: oidc.ErrAuthorizationPending(), + }, + { + name: "slow down", + args: args{ + ctx: exceededCtx, + clientID: "native", + deviceCode: "ok", + }, + wantErr: oidc.ErrSlowDown(), + }, + { + name: "wrong client", + args: args{ + ctx: context.Background(), + clientID: "foo", + deviceCode: "ok", + }, + wantErr: oidc.ErrAccessDenied(), + }, + { + name: "denied", + args: args{ + ctx: context.Background(), + clientID: "native", + deviceCode: "denied", + }, + want: &op.DeviceAuthorizationState{ + ClientID: "native", + Scopes: []string{"foo"}, + Expires: now.Add(time.Minute), + Denied: true, + }, + wantErr: oidc.ErrAccessDenied(), + }, + { + name: "completed", + args: args{ + ctx: context.Background(), + clientID: "native", + deviceCode: "completed", + }, + want: &op.DeviceAuthorizationState{ + ClientID: "native", + Scopes: []string{"foo"}, + Expires: now.Add(time.Minute), + Subject: "tim", + Done: true, + }, + }, + { + name: "expired", + args: args{ + ctx: context.Background(), + clientID: "native", + deviceCode: "expired", + }, + want: &op.DeviceAuthorizationState{ + ClientID: "native", + Scopes: []string{"foo"}, + Expires: now.Add(-time.Minute), + }, + wantErr: oidc.ErrExpiredDeviceCode(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := op.CheckDeviceAuthorizationState(tt.args.ctx, tt.args.clientID, tt.args.deviceCode, testProvider) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 9a25afc..26f89eb 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -44,6 +44,7 @@ func CreateDiscoveryConfig(r *http.Request, config Configuration, storage Discov RevocationEndpoint: config.RevocationEndpoint().Absolute(issuer), EndSessionEndpoint: config.EndSessionEndpoint().Absolute(issuer), JwksURI: config.KeysEndpoint().Absolute(issuer), + DeviceAuthorizationEndpoint: config.DeviceAuthorizationEndpoint().Absolute(issuer), ScopesSupported: Scopes(config), ResponseTypesSupported: ResponseTypes(config), GrantTypesSupported: GrantTypes(config), @@ -92,6 +93,9 @@ func GrantTypes(c Configuration) []oidc.GrantType { if c.GrantTypeJWTAuthorizationSupported() { grantTypes = append(grantTypes, oidc.GrantTypeBearer) } + if c.GrantTypeDeviceCodeSupported() { + grantTypes = append(grantTypes, oidc.GrantTypeDeviceCode) + } return grantTypes } diff --git a/pkg/op/discovery_test.go b/pkg/op/discovery_test.go index e1b07dd..2d0b8af 100644 --- a/pkg/op/discovery_test.go +++ b/pkg/op/discovery_test.go @@ -131,6 +131,7 @@ func Test_GrantTypes(t *testing.T) { c.EXPECT().GrantTypeTokenExchangeSupported().Return(false) c.EXPECT().GrantTypeJWTAuthorizationSupported().Return(false) c.EXPECT().GrantTypeClientCredentialsSupported().Return(false) + c.EXPECT().GrantTypeDeviceCodeSupported().Return(false) return c }(), }, @@ -148,6 +149,7 @@ func Test_GrantTypes(t *testing.T) { c.EXPECT().GrantTypeTokenExchangeSupported().Return(true) c.EXPECT().GrantTypeJWTAuthorizationSupported().Return(true) c.EXPECT().GrantTypeClientCredentialsSupported().Return(true) + c.EXPECT().GrantTypeDeviceCodeSupported().Return(false) return c }(), }, diff --git a/pkg/op/mock/configuration.mock.go b/pkg/op/mock/configuration.mock.go index fc3158a..44b5ceb 100644 --- a/pkg/op/mock/configuration.mock.go +++ b/pkg/op/mock/configuration.mock.go @@ -92,6 +92,34 @@ func (mr *MockConfigurationMockRecorder) CodeMethodS256Supported() *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CodeMethodS256Supported", reflect.TypeOf((*MockConfiguration)(nil).CodeMethodS256Supported)) } +// DeviceAuthorization mocks base method. +func (m *MockConfiguration) DeviceAuthorization() op.DeviceAuthorizationConfig { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeviceAuthorization") + ret0, _ := ret[0].(op.DeviceAuthorizationConfig) + return ret0 +} + +// DeviceAuthorization indicates an expected call of DeviceAuthorization. +func (mr *MockConfigurationMockRecorder) DeviceAuthorization() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeviceAuthorization", reflect.TypeOf((*MockConfiguration)(nil).DeviceAuthorization)) +} + +// DeviceAuthorizationEndpoint mocks base method. +func (m *MockConfiguration) DeviceAuthorizationEndpoint() op.Endpoint { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeviceAuthorizationEndpoint") + ret0, _ := ret[0].(op.Endpoint) + return ret0 +} + +// DeviceAuthorizationEndpoint indicates an expected call of DeviceAuthorizationEndpoint. +func (mr *MockConfigurationMockRecorder) DeviceAuthorizationEndpoint() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeviceAuthorizationEndpoint", reflect.TypeOf((*MockConfiguration)(nil).DeviceAuthorizationEndpoint)) +} + // EndSessionEndpoint mocks base method. func (m *MockConfiguration) EndSessionEndpoint() op.Endpoint { m.ctrl.T.Helper() @@ -120,6 +148,20 @@ func (mr *MockConfigurationMockRecorder) GrantTypeClientCredentialsSupported() * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeClientCredentialsSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeClientCredentialsSupported)) } +// GrantTypeDeviceCodeSupported mocks base method. +func (m *MockConfiguration) GrantTypeDeviceCodeSupported() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GrantTypeDeviceCodeSupported") + ret0, _ := ret[0].(bool) + return ret0 +} + +// GrantTypeDeviceCodeSupported indicates an expected call of GrantTypeDeviceCodeSupported. +func (mr *MockConfigurationMockRecorder) GrantTypeDeviceCodeSupported() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeDeviceCodeSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeDeviceCodeSupported)) +} + // GrantTypeJWTAuthorizationSupported mocks base method. func (m *MockConfiguration) GrantTypeJWTAuthorizationSupported() bool { m.ctrl.T.Helper() @@ -358,6 +400,20 @@ func (mr *MockConfigurationMockRecorder) TokenEndpointSigningAlgorithmsSupported return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TokenEndpointSigningAlgorithmsSupported", reflect.TypeOf((*MockConfiguration)(nil).TokenEndpointSigningAlgorithmsSupported)) } +// UserCodeFormEndpoint mocks base method. +func (m *MockConfiguration) UserCodeFormEndpoint() op.Endpoint { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UserCodeFormEndpoint") + ret0, _ := ret[0].(op.Endpoint) + return ret0 +} + +// UserCodeFormEndpoint indicates an expected call of UserCodeFormEndpoint. +func (mr *MockConfigurationMockRecorder) UserCodeFormEndpoint() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserCodeFormEndpoint", reflect.TypeOf((*MockConfiguration)(nil).UserCodeFormEndpoint)) +} + // UserinfoEndpoint mocks base method. func (m *MockConfiguration) UserinfoEndpoint() op.Endpoint { m.ctrl.T.Helper() diff --git a/pkg/op/op.go b/pkg/op/op.go index 699fb45..2859722 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -27,17 +27,19 @@ const ( defaultRevocationEndpoint = "revoke" defaultEndSessionEndpoint = "end_session" defaultKeysEndpoint = "keys" + defaultDeviceAuthzEndpoint = "/device_authorization" ) var ( DefaultEndpoints = &endpoints{ - Authorization: NewEndpoint(defaultAuthorizationEndpoint), - Token: NewEndpoint(defaultTokenEndpoint), - Introspection: NewEndpoint(defaultIntrospectEndpoint), - Userinfo: NewEndpoint(defaultUserinfoEndpoint), - Revocation: NewEndpoint(defaultRevocationEndpoint), - EndSession: NewEndpoint(defaultEndSessionEndpoint), - JwksURI: NewEndpoint(defaultKeysEndpoint), + Authorization: NewEndpoint(defaultAuthorizationEndpoint), + Token: NewEndpoint(defaultTokenEndpoint), + Introspection: NewEndpoint(defaultIntrospectEndpoint), + Userinfo: NewEndpoint(defaultUserinfoEndpoint), + Revocation: NewEndpoint(defaultRevocationEndpoint), + EndSession: NewEndpoint(defaultEndSessionEndpoint), + JwksURI: NewEndpoint(defaultKeysEndpoint), + DeviceAuthorization: NewEndpoint(defaultDeviceAuthzEndpoint), } defaultCORSOptions = cors.Options{ @@ -95,6 +97,7 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router router.HandleFunc(o.RevocationEndpoint().Relative(), revocationHandler(o)) router.HandleFunc(o.EndSessionEndpoint().Relative(), endSessionHandler(o)) router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o.Storage())) + router.HandleFunc(o.DeviceAuthorizationEndpoint().Relative(), DeviceAuthorizationHandler(o)) return router } @@ -118,17 +121,19 @@ type Config struct { GrantTypeRefreshToken bool RequestObjectSupported bool SupportedUILocales []language.Tag + DeviceAuthorization DeviceAuthorizationConfig } type endpoints struct { - Authorization Endpoint - Token Endpoint - Introspection Endpoint - Userinfo Endpoint - Revocation Endpoint - EndSession Endpoint - CheckSessionIframe Endpoint - JwksURI Endpoint + Authorization Endpoint + Token Endpoint + Introspection Endpoint + Userinfo Endpoint + Revocation Endpoint + EndSession Endpoint + CheckSessionIframe Endpoint + JwksURI Endpoint + DeviceAuthorization Endpoint } // NewOpenIDProvider creates a provider. The provider provides (with HttpHandler()) @@ -145,6 +150,7 @@ type endpoints struct { // /revoke // /end_session // /keys +// /device_authorization // // This does not include login. Login is handled with a redirect that includes the // request ID. The redirect for logins is specified per-client by Client.LoginURL(). @@ -242,6 +248,10 @@ func (o *Provider) EndSessionEndpoint() Endpoint { return o.endpoints.EndSession } +func (o *Provider) DeviceAuthorizationEndpoint() Endpoint { + return o.endpoints.DeviceAuthorization +} + func (o *Provider) KeysEndpoint() Endpoint { return o.endpoints.JwksURI } @@ -275,6 +285,11 @@ func (o *Provider) GrantTypeJWTAuthorizationSupported() bool { return true } +func (o *Provider) GrantTypeDeviceCodeSupported() bool { + _, ok := o.storage.(DeviceAuthorizationStorage) + return ok +} + func (o *Provider) IntrospectionAuthMethodPrivateKeyJWTSupported() bool { return true } @@ -308,6 +323,10 @@ func (o *Provider) SupportedUILocales() []language.Tag { return o.config.SupportedUILocales } +func (o *Provider) DeviceAuthorization() DeviceAuthorizationConfig { + return o.config.DeviceAuthorization +} + func (o *Provider) Storage() Storage { return o.storage } diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 1e19c76..ebab1c3 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -151,3 +151,50 @@ type EndSessionRequest struct { ClientID string RedirectURI string } + +var ErrDuplicateUserCode = errors.New("user code already exists") + +type DeviceAuthorizationState struct { + ClientID string + Scopes []string + Expires time.Time + Done bool + Subject string + Denied bool +} + +type DeviceAuthorizationStorage interface { + // StoreDeviceAuthorizationRequest stores a new device authorization request in the database. + // User code will be used by the user to complete the login flow and must be unique. + // ErrDuplicateUserCode signals the caller should try again with a new code. + // + // Note that user codes are low entropy keys and when many exist in the + // database, the change for collisions increases. Therefore implementers + // of this interface must make sure that user codes of expired authentication flows are purged, + // after some time. + StoreDeviceAuthorization(ctx context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes []string) error + + // GetDeviceAuthorizatonState returns the current state of the device authorization flow in the database. + // The method is polled untill the the authorization is eighter Completed, Expired or Denied. + GetDeviceAuthorizatonState(ctx context.Context, clientID, deviceCode string) (*DeviceAuthorizationState, error) + + // GetDeviceAuthorizationByUserCode resturn the current state of the device authorization flow, + // identified by the user code. + GetDeviceAuthorizationByUserCode(ctx context.Context, userCode string) (*DeviceAuthorizationState, error) + + // CompleteDeviceAuthorization marks a device authorization entry as Completed, + // identified by userCode. The Subject is added to the state, so that + // GetDeviceAuthorizatonState can use it to create a new Access Token. + CompleteDeviceAuthorization(ctx context.Context, userCode, subject string) error + + // DenyDeviceAuthorization marks a device authorization entry as Denied. + DenyDeviceAuthorization(ctx context.Context, userCode string) error +} + +func assertDeviceStorage(s Storage) (DeviceAuthorizationStorage, error) { + storage, ok := s.(DeviceAuthorizationStorage) + if !ok { + return nil, oidc.ErrUnsupportedGrantType().WithDescription("device_code grant not supported") + } + return storage, nil +} diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go index dfc8954..e7ca7c4 100644 --- a/pkg/op/token_intospection.go +++ b/pkg/op/token_intospection.go @@ -4,7 +4,6 @@ import ( "context" "errors" "net/http" - "net/url" httphelper "github.com/zitadel/oidc/v2/pkg/http" "github.com/zitadel/oidc/v2/pkg/oidc" @@ -50,38 +49,19 @@ func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspecto } func ParseTokenIntrospectionRequest(r *http.Request, introspector Introspector) (token, clientID string, err error) { - err = r.ParseForm() + clientID, authenticated, err := ClientIDFromRequest(r, introspector) if err != nil { - return "", "", errors.New("unable to parse request") + return "", "", err } - req := new(struct { - oidc.IntrospectionRequest - oidc.ClientAssertionParams - }) + if !authenticated { + return "", "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials) + } + + req := new(oidc.IntrospectionRequest) err = introspector.Decoder().Decode(req, r.Form) if err != nil { return "", "", errors.New("unable to parse request") } - if introspectorJWTProfile, ok := introspector.(IntrospectorJWTProfile); ok && req.ClientAssertion != "" { - profile, err := VerifyJWTAssertion(r.Context(), req.ClientAssertion, introspectorJWTProfile.JWTProfileVerifier(r.Context())) - if err == nil { - return req.Token, profile.Issuer, nil - } - } - clientID, clientSecret, ok := r.BasicAuth() - if ok { - clientID, err = url.QueryUnescape(clientID) - if err != nil { - return "", "", errors.New("invalid basic auth header") - } - clientSecret, err = url.QueryUnescape(clientSecret) - if err != nil { - return "", "", errors.New("invalid basic auth header") - } - if err := introspector.Storage().AuthorizeClientIDSecret(r.Context(), clientID, clientSecret); err != nil { - return "", "", err - } - return req.Token, clientID, nil - } - return "", "", errors.New("invalid authorization") + + return req.Token, clientID, nil } diff --git a/pkg/op/token_request.go b/pkg/op/token_request.go index 3d65ea0..b9e9805 100644 --- a/pkg/op/token_request.go +++ b/pkg/op/token_request.go @@ -19,6 +19,7 @@ type Exchanger interface { GrantTypeTokenExchangeSupported() bool GrantTypeJWTAuthorizationSupported() bool GrantTypeClientCredentialsSupported() bool + GrantTypeDeviceCodeSupported() bool AccessTokenVerifier(context.Context) AccessTokenVerifier IDTokenHintVerifier(context.Context) IDTokenHintVerifier } @@ -56,6 +57,11 @@ func Exchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { ClientCredentialsExchange(w, r, exchanger) return } + case string(oidc.GrantTypeDeviceCode): + if exchanger.GrantTypeDeviceCodeSupported() { + DeviceAccessToken(w, r, exchanger) + return + } case "": RequestError(w, r, oidc.ErrInvalidRequest().WithDescription("grant_type missing")) return From f3eae0f32925b0db827e9473a5a6d9ef6caee80f Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Tue, 28 Feb 2023 12:44:33 -0800 Subject: [PATCH 179/502] breaking change: add rp/RelyingParty.GetRevokeEndpoint --- NEXT_RELEASE.md | 1 - pkg/client/rp/relying_party.go | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/NEXT_RELEASE.md b/NEXT_RELEASE.md index 91f7f5d..4bde900 100644 --- a/NEXT_RELEASE.md +++ b/NEXT_RELEASE.md @@ -1,7 +1,6 @@ # Backwards-incompatible changes to be made in the next major release -- Add `rp/RelyingParty.GetRevokeEndpoint` - Rename `op/OpStorage.GetKeyByIDAndUserID` to `op/OpStorage.GetKeyByIDAndClientID` - Add `CanRefreshTokenInfo` (`GetRefreshTokenInfo()`) to `op.Storage` diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 96fe219..5bd3558 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -54,7 +54,7 @@ type RelyingParty interface { GetEndSessionEndpoint() string // GetRevokeEndpoint returns the endpoint to revoke a specific token - // "GetRevokeEndpoint() string" will be added in a future release + GetRevokeEndpoint() string // UserinfoEndpoint returns the userinfo UserinfoEndpoint() string From f447b9b6d461168d2643c4b9f12bd8cf99f1fbe1 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Tue, 28 Feb 2023 15:49:24 -0800 Subject: [PATCH 180/502] breaking change: Add GetRefreshTokenInfo() to op.Storage --- NEXT_RELEASE.md | 1 - example/server/storage/storage.go | 12 +++++++++++- example/server/storage/storage_dynamic.go | 10 ++++++++++ go.mod | 1 + go.sum | 12 ++++++++++++ pkg/op/mock/storage.mock.go | 16 ++++++++++++++++ pkg/op/storage.go | 17 +++++++---------- pkg/op/token_revocation.go | 4 ++-- 8 files changed, 59 insertions(+), 14 deletions(-) diff --git a/NEXT_RELEASE.md b/NEXT_RELEASE.md index 4bde900..e113dce 100644 --- a/NEXT_RELEASE.md +++ b/NEXT_RELEASE.md @@ -2,5 +2,4 @@ # Backwards-incompatible changes to be made in the next major release - Rename `op/OpStorage.GetKeyByIDAndUserID` to `op/OpStorage.GetKeyByIDAndClientID` -- Add `CanRefreshTokenInfo` (`GetRefreshTokenInfo()`) to `op.Storage` diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index b49ce1b..08efeb3 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -327,6 +327,16 @@ func (s *Storage) TerminateSession(ctx context.Context, userID string, clientID return nil } +// GetRefreshTokenInfo looks up a refresh token and returns the token id and user id. +// If given something that is not a refresh token, it must return error. +func (s *Storage) GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error) { + refreshToken, ok := s.refreshTokens[token] + if !ok { + return "", "", op.ErrInvalidRefreshToken + } + return refreshToken.UserID, refreshToken.ID, nil +} + // RevokeToken implements the op.Storage interface // it will be called after parsing and validation of the token revocation request func (s *Storage) RevokeToken(ctx context.Context, tokenIDOrToken string, userID string, clientID string) *oidc.Error { @@ -384,7 +394,7 @@ func (s *Storage) KeySet(ctx context.Context) ([]op.Key, error) { // so it will directly use its public key // // when using key rotation you typically would store the public keys alongside the private keys in your database - //and give both of them an expiration date, with the public key having a longer lifetime + // and give both of them an expiration date, with the public key having a longer lifetime return []op.Key{&publicKey{s.signingKey}}, nil } diff --git a/example/server/storage/storage_dynamic.go b/example/server/storage/storage_dynamic.go index ec6a92e..b8051fa 100644 --- a/example/server/storage/storage_dynamic.go +++ b/example/server/storage/storage_dynamic.go @@ -126,6 +126,16 @@ func (s *multiStorage) TerminateSession(ctx context.Context, userID string, clie return storage.TerminateSession(ctx, userID, clientID) } +// GetRefreshTokenInfo looks up a refresh token and returns the token id and user id. +// If given something that is not a refresh token, it must return error. +func (s *multiStorage) GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error) { + storage, err := s.storageFromContext(ctx) + if err != nil { + return "", "", err + } + return storage.GetRefreshTokenInfo(ctx, clientID, token) +} + // RevokeToken implements the op.Storage interface // it will be called after parsing and validation of the token revocation request func (s *multiStorage) RevokeToken(ctx context.Context, token string, userID string, clientID string) *oidc.Error { diff --git a/go.mod b/go.mod index 2691e57..d3e1234 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/zitadel/oidc/v2 go 1.16 require ( + github.com/dmarkham/enumer v1.5.7 // indirect github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.2 // indirect github.com/google/go-github/v31 v31.0.0 diff --git a/go.sum b/go.sum index c73eb9d..8ce0b62 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX 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/dmarkham/enumer v1.5.7 h1:xYJA/lGoniiuhZLASBUbpPjScUslfyDHUAMczeflCeg= +github.com/dmarkham/enumer v1.5.7/go.mod h1:eAawajOQnFBxf0NndBKgbqJImkHytg3eFEngUovqgo8= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -125,6 +127,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pascaldekloe/name v1.0.0 h1:n7LKFgHixETzxpRv2R77YgPUFo85QHGZKrdaYm7eY5U= +github.com/pascaldekloe/name v1.0.0/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -146,6 +150,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -190,6 +195,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -219,6 +225,7 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -265,8 +272,10 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= @@ -278,6 +287,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= @@ -325,6 +335,8 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index 58cc2a0..c01137d 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -189,6 +189,22 @@ func (mr *MockStorageMockRecorder) GetPrivateClaimsFromScopes(arg0, arg1, arg2, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrivateClaimsFromScopes", reflect.TypeOf((*MockStorage)(nil).GetPrivateClaimsFromScopes), arg0, arg1, arg2, arg3) } +// GetRefreshTokenInfo mocks base method. +func (m *MockStorage) GetRefreshTokenInfo(arg0 context.Context, arg1, arg2 string) (string, string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRefreshTokenInfo", arg0, arg1, arg2) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetRefreshTokenInfo indicates an expected call of GetRefreshTokenInfo. +func (mr *MockStorageMockRecorder) GetRefreshTokenInfo(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRefreshTokenInfo", reflect.TypeOf((*MockStorage)(nil).GetRefreshTokenInfo), arg0, arg1, arg2) +} + // Health mocks base method. func (m *MockStorage) Health(arg0 context.Context) error { m.ctrl.T.Helper() diff --git a/pkg/op/storage.go b/pkg/op/storage.go index ebab1c3..c87fac3 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -48,9 +48,15 @@ type AuthStorage interface { // RevokeToken should revoke a token. In the situation that the original request was to // revoke an access token, then tokenOrTokenID will be a tokenID and userID will be set // but if the original request was for a refresh token, then userID will be empty and - // tokenOrTokenID will be the refresh token, not its ID. + // tokenOrTokenID will be the refresh token, not its ID. RevokeToken depends upon GetRefreshTokenInfo + // to get information from refresh tokens that are not either ":" strings + // nor JWTs. RevokeToken(ctx context.Context, tokenOrTokenID string, userID string, clientID string) *oidc.Error + // GetRefreshTokenInfo must return ErrInvalidRefreshToken when presented + // with a token that is not a refresh token. + GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error) + SigningKey(context.Context) (SigningKey, error) SignatureAlgorithms(context.Context) ([]jose.SignatureAlgorithm, error) KeySet(context.Context) ([]Key, error) @@ -100,15 +106,6 @@ type TokenExchangeTokensVerifierStorage interface { VerifyExchangeActorToken(ctx context.Context, token string, tokenType oidc.TokenType) (tokenIDOrToken string, actor string, tokenClaims map[string]interface{}, err error) } -// CanRefreshTokenInfo is an optional additional interface that Storage can support. -// Supporting CanRefreshTokenInfo is required to be able to (revoke) a refresh token that -// is neither an encrypted string of : nor a JWT. -type CanRefreshTokenInfo interface { - // GetRefreshTokenInfo must return ErrInvalidRefreshToken when presented - // with a token that is not a refresh token. - GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error) -} - var ErrInvalidRefreshToken = errors.New("invalid_refresh_token") type OPStorage interface { diff --git a/pkg/op/token_revocation.go b/pkg/op/token_revocation.go index 7dbd4a7..33978f5 100644 --- a/pkg/op/token_revocation.go +++ b/pkg/op/token_revocation.go @@ -39,8 +39,8 @@ func Revoke(w http.ResponseWriter, r *http.Request, revoker Revoker) { } var subject string doDecrypt := true - if canRefreshInfo, ok := revoker.Storage().(CanRefreshTokenInfo); ok && tokenTypeHint != "access_token" { - userID, tokenID, err := canRefreshInfo.GetRefreshTokenInfo(r.Context(), clientID, token) + if tokenTypeHint != "access_token" { + userID, tokenID, err := revoker.Storage().GetRefreshTokenInfo(r.Context(), clientID, token) if err != nil { // An invalid refresh token means that we'll try other things (leaving doDecrypt==true) if !errors.Is(err, ErrInvalidRefreshToken) { From 0c74bd51db3b7db7a065862ac6d4a5324a4a70fc Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Tue, 28 Feb 2023 16:15:25 -0800 Subject: [PATCH 181/502] breaking change: rename GetKeyByIDAndUserID -> GetKeyByIDAndClientID --- NEXT_RELEASE.md | 1 - example/server/storage/storage.go | 4 ++-- example/server/storage/storage_dynamic.go | 6 +++--- pkg/op/mock/storage.mock.go | 12 ++++++------ pkg/op/storage.go | 5 +---- pkg/op/verifier_jwt_profile.go | 4 ++-- 6 files changed, 14 insertions(+), 18 deletions(-) diff --git a/NEXT_RELEASE.md b/NEXT_RELEASE.md index e113dce..f515c40 100644 --- a/NEXT_RELEASE.md +++ b/NEXT_RELEASE.md @@ -1,5 +1,4 @@ # Backwards-incompatible changes to be made in the next major release -- Rename `op/OpStorage.GetKeyByIDAndUserID` to `op/OpStorage.GetKeyByIDAndClientID` diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index 08efeb3..2794783 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -510,9 +510,9 @@ func (s *Storage) getPrivateClaimsFromScopes(ctx context.Context, userID, client return claims, nil } -// GetKeyByIDAndUserID implements the op.Storage interface +// GetKeyByIDAndClientID implements the op.Storage interface // it will be called to validate the signatures of a JWT (JWT Profile Grant and Authentication) -func (s *Storage) GetKeyByIDAndUserID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error) { +func (s *Storage) GetKeyByIDAndClientID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error) { s.lock.Lock() defer s.lock.Unlock() service, ok := s.services[clientID] diff --git a/example/server/storage/storage_dynamic.go b/example/server/storage/storage_dynamic.go index b8051fa..d424a89 100644 --- a/example/server/storage/storage_dynamic.go +++ b/example/server/storage/storage_dynamic.go @@ -236,14 +236,14 @@ func (s *multiStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, c return storage.GetPrivateClaimsFromScopes(ctx, userID, clientID, scopes) } -// GetKeyByIDAndUserID implements the op.Storage interface +// GetKeyByIDAndClientID implements the op.Storage interface // it will be called to validate the signatures of a JWT (JWT Profile Grant and Authentication) -func (s *multiStorage) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) { +func (s *multiStorage) GetKeyByIDAndClientID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) { storage, err := s.storageFromContext(ctx) if err != nil { return nil, err } - return storage.GetKeyByIDAndUserID(ctx, keyID, userID) + return storage.GetKeyByIDAndClientID(ctx, keyID, userID) } // ValidateJWTProfileScopes implements the op.Storage interface diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index c01137d..fc0c358 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -159,19 +159,19 @@ func (mr *MockStorageMockRecorder) GetClientByClientID(arg0, arg1 interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClientByClientID", reflect.TypeOf((*MockStorage)(nil).GetClientByClientID), arg0, arg1) } -// GetKeyByIDAndUserID mocks base method. -func (m *MockStorage) GetKeyByIDAndUserID(arg0 context.Context, arg1, arg2 string) (*jose.JSONWebKey, error) { +// GetKeyByIDAndClientID mocks base method. +func (m *MockStorage) GetKeyByIDAndClientID(arg0 context.Context, arg1, arg2 string) (*jose.JSONWebKey, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetKeyByIDAndUserID", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "GetKeyByIDAndClientID", arg0, arg1, arg2) ret0, _ := ret[0].(*jose.JSONWebKey) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetKeyByIDAndUserID indicates an expected call of GetKeyByIDAndUserID. -func (mr *MockStorageMockRecorder) GetKeyByIDAndUserID(arg0, arg1, arg2 interface{}) *gomock.Call { +// GetKeyByIDAndClientID indicates an expected call of GetKeyByIDAndClientID. +func (mr *MockStorageMockRecorder) GetKeyByIDAndClientID(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeyByIDAndUserID", reflect.TypeOf((*MockStorage)(nil).GetKeyByIDAndUserID), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeyByIDAndClientID", reflect.TypeOf((*MockStorage)(nil).GetKeyByIDAndClientID), arg0, arg1, arg2) } // GetPrivateClaimsFromScopes mocks base method. diff --git a/pkg/op/storage.go b/pkg/op/storage.go index c87fac3..8ba1946 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -115,10 +115,7 @@ type OPStorage interface { SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, clientID string) error GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) - - // GetKeyByIDAndUserID is mis-named. It does not pass userID. Instead - // it passes the clientID. - GetKeyByIDAndUserID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error) + GetKeyByIDAndClientID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error) ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error) } diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index 9befb64..4d83c59 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -104,7 +104,7 @@ func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerif } type jwtProfileKeyStorage interface { - GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) + GetKeyByIDAndClientID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) } func SubjectIsIssuer(request *oidc.JWTTokenRequest) error { @@ -122,7 +122,7 @@ type jwtProfileKeySet struct { // VerifySignature implements oidc.KeySet by getting the public key from Storage implementation func (k *jwtProfileKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) { keyID, _ := oidc.GetKeyIDAndAlg(jws) - key, err := k.storage.GetKeyByIDAndUserID(ctx, keyID, k.clientID) + key, err := k.storage.GetKeyByIDAndClientID(ctx, keyID, k.clientID) if err != nil { return nil, fmt.Errorf("error fetching keys: %w", err) } From ad76a7cb072bc3dd4e2370f3ce28ba36df298b55 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Tue, 28 Feb 2023 16:15:44 -0800 Subject: [PATCH 182/502] remove empty NEXT_RELEASE.md --- NEXT_RELEASE.md | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 NEXT_RELEASE.md diff --git a/NEXT_RELEASE.md b/NEXT_RELEASE.md deleted file mode 100644 index f515c40..0000000 --- a/NEXT_RELEASE.md +++ /dev/null @@ -1,4 +0,0 @@ - -# Backwards-incompatible changes to be made in the next major release - - From 2d4ce6fde3353fcbd0ebd73f4bc4c9786859cea5 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Wed, 1 Mar 2023 12:39:12 -0800 Subject: [PATCH 183/502] go mod tidy --- go.mod | 1 - go.sum | 12 ------------ 2 files changed, 13 deletions(-) diff --git a/go.mod b/go.mod index d3e1234..2691e57 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/zitadel/oidc/v2 go 1.16 require ( - github.com/dmarkham/enumer v1.5.7 // indirect github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.2 // indirect github.com/google/go-github/v31 v31.0.0 diff --git a/go.sum b/go.sum index 8ce0b62..c73eb9d 100644 --- a/go.sum +++ b/go.sum @@ -42,8 +42,6 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX 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/dmarkham/enumer v1.5.7 h1:xYJA/lGoniiuhZLASBUbpPjScUslfyDHUAMczeflCeg= -github.com/dmarkham/enumer v1.5.7/go.mod h1:eAawajOQnFBxf0NndBKgbqJImkHytg3eFEngUovqgo8= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -127,8 +125,6 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/pascaldekloe/name v1.0.0 h1:n7LKFgHixETzxpRv2R77YgPUFo85QHGZKrdaYm7eY5U= -github.com/pascaldekloe/name v1.0.0/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -150,7 +146,6 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -195,7 +190,6 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -225,7 +219,6 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -272,10 +265,8 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= @@ -287,7 +278,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= @@ -335,8 +325,6 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 1eb4ee1c8e5dae9c9f3d332d617d22d42b5db68b Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Wed, 1 Mar 2023 12:43:09 -0800 Subject: [PATCH 184/502] auto install things for "go generate" and then clean up afterwards --- pkg/op/client.go | 1 + pkg/op/mock/configuration.mock.go | 14 -------------- pkg/op/mock/generate.go | 1 + 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/pkg/op/client.go b/pkg/op/client.go index 1f5e1c9..f1d106b 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -13,6 +13,7 @@ import ( //go:generate go get github.com/dmarkham/enumer //go:generate go run github.com/dmarkham/enumer -linecomment -sql -json -text -yaml -gqlgen -type=ApplicationType,AccessTokenType +//go:generate go mod tidy const ( ApplicationTypeWeb ApplicationType = iota // web diff --git a/pkg/op/mock/configuration.mock.go b/pkg/op/mock/configuration.mock.go index 44b5ceb..fe7d4da 100644 --- a/pkg/op/mock/configuration.mock.go +++ b/pkg/op/mock/configuration.mock.go @@ -400,20 +400,6 @@ func (mr *MockConfigurationMockRecorder) TokenEndpointSigningAlgorithmsSupported return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TokenEndpointSigningAlgorithmsSupported", reflect.TypeOf((*MockConfiguration)(nil).TokenEndpointSigningAlgorithmsSupported)) } -// UserCodeFormEndpoint mocks base method. -func (m *MockConfiguration) UserCodeFormEndpoint() op.Endpoint { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UserCodeFormEndpoint") - ret0, _ := ret[0].(op.Endpoint) - return ret0 -} - -// UserCodeFormEndpoint indicates an expected call of UserCodeFormEndpoint. -func (mr *MockConfigurationMockRecorder) UserCodeFormEndpoint() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserCodeFormEndpoint", reflect.TypeOf((*MockConfiguration)(nil).UserCodeFormEndpoint)) -} - // UserinfoEndpoint mocks base method. func (m *MockConfiguration) UserinfoEndpoint() op.Endpoint { m.ctrl.T.Helper() diff --git a/pkg/op/mock/generate.go b/pkg/op/mock/generate.go index 0066571..ca288d2 100644 --- a/pkg/op/mock/generate.go +++ b/pkg/op/mock/generate.go @@ -1,5 +1,6 @@ package mock +//go:generate go install github.com/golang/mock/mockgen@v1.6.0 //go:generate mockgen -package mock -destination ./storage.mock.go github.com/zitadel/oidc/v2/pkg/op Storage //go:generate mockgen -package mock -destination ./authorizer.mock.go github.com/zitadel/oidc/v2/pkg/op Authorizer //go:generate mockgen -package mock -destination ./client.mock.go github.com/zitadel/oidc/v2/pkg/op Client From fc1a80d2749145d01b704965ce709eb723aa0b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Thu, 2 Mar 2023 15:24:13 +0200 Subject: [PATCH 185/502] chore: enable github actions for next branch (#298) --- .github/workflows/codeql-analysis.yml | 4 ++-- .github/workflows/release.yml | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 85ea2ca..d2bae79 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,10 +2,10 @@ name: "Code scanning - action" on: push: - branches: [main, ] + branches: [main,next] pull_request: # The branches below must be a subset of the branches above - branches: [main] + branches: [main,next] schedule: - cron: '0 11 * * 0' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d97d41a..9509826 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,7 @@ on: push: branches: - main + - next tags-ignore: - '**' pull_request: @@ -31,7 +32,7 @@ jobs: release: runs-on: ubuntu-20.04 needs: [test] - if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' }} + if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: From 4dca29f1f9635a3879f400c8b5d18db44ee12565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Thu, 2 Mar 2023 15:24:44 +0200 Subject: [PATCH 186/502] fix: use the same schema encoder everywhere (#299) properly register SpaceDelimitedArray for all instances of schema.Encoder inside the oidc framework. Closes #295 --- pkg/client/client.go | 10 +--------- pkg/oidc/types.go | 12 ++++++++++++ pkg/oidc/types_test.go | 19 +++++++++++++++++++ pkg/op/device_test.go | 3 --- pkg/op/op.go | 2 +- 5 files changed, 33 insertions(+), 13 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index b9ae008..ebe1442 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -8,11 +8,9 @@ import ( "io" "net/http" "net/url" - "reflect" "strings" "time" - "github.com/gorilla/schema" "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" @@ -21,13 +19,7 @@ import ( "github.com/zitadel/oidc/v2/pkg/oidc" ) -var Encoder = func() httphelper.Encoder { - e := schema.NewEncoder() - e.RegisterEncoder(oidc.SpaceDelimitedArray{}, func(value reflect.Value) string { - return value.Interface().(oidc.SpaceDelimitedArray).Encode() - }) - return e -}() +var Encoder = httphelper.Encoder(oidc.NewEncoder()) // Discover calls the discovery endpoint of the provided issuer and returns its configuration // It accepts an optional argument "wellknownUrl" which can be used to overide the dicovery endpoint url diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index 1260798..21b6fba 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -4,9 +4,11 @@ import ( "database/sql/driver" "encoding/json" "fmt" + "reflect" "strings" "time" + "github.com/gorilla/schema" "golang.org/x/text/language" "gopkg.in/square/go-jose.v2" ) @@ -125,6 +127,16 @@ func (s SpaceDelimitedArray) Value() (driver.Value, error) { return strings.Join(s, " "), nil } +// NewEncoder returns a schema Encoder with +// a registered encoder for SpaceDelimitedArray. +func NewEncoder() *schema.Encoder { + e := schema.NewEncoder() + e.RegisterEncoder(SpaceDelimitedArray{}, func(value reflect.Value) string { + return value.Interface().(SpaceDelimitedArray).Encode() + }) + return e +} + type Time time.Time func (t *Time) UnmarshalJSON(data []byte) error { diff --git a/pkg/oidc/types_test.go b/pkg/oidc/types_test.go index 6c62c40..74323da 100644 --- a/pkg/oidc/types_test.go +++ b/pkg/oidc/types_test.go @@ -3,10 +3,12 @@ package oidc import ( "bytes" "encoding/json" + "net/url" "strconv" "strings" "testing" + "github.com/gorilla/schema" "github.com/stretchr/testify/assert" "golang.org/x/text/language" ) @@ -335,3 +337,20 @@ func TestSpaceDelimitatedArray_ValuerNil(t *testing.T) { assert.Equal(t, SpaceDelimitedArray(nil), reversed, "scan nil") } } + +func TestNewEncoder(t *testing.T) { + type request struct { + Scopes SpaceDelimitedArray `schema:"scope"` + } + a := request{ + Scopes: SpaceDelimitedArray{"foo", "bar"}, + } + + values := make(url.Values) + NewEncoder().Encode(a, values) + assert.Equal(t, url.Values{"scope": []string{"foo bar"}}, values) + + var b request + schema.NewDecoder().Decode(&b, values) + assert.Equal(t, a, b) +} diff --git a/pkg/op/device_test.go b/pkg/op/device_test.go index ca68759..b3ac89d 100644 --- a/pkg/op/device_test.go +++ b/pkg/op/device_test.go @@ -98,8 +98,6 @@ func TestParseDeviceCodeRequest(t *testing.T) { name: "empty request", wantErr: true, }, - /* decoding a SpaceDelimitedArray is broken - https://github.com/zitadel/oidc/issues/295 { name: "success", req: &oidc.DeviceAuthorizationRequest{ @@ -107,7 +105,6 @@ func TestParseDeviceCodeRequest(t *testing.T) { ClientID: "web", }, }, - */ } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/op/op.go b/pkg/op/op.go index 2859722..bb45425 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -189,7 +189,7 @@ func newProvider(ctx context.Context, config *Config, storage Storage, issuer fu o.decoder = schema.NewDecoder() o.decoder.IgnoreUnknownKeys(true) - o.encoder = schema.NewEncoder() + o.encoder = oidc.NewEncoder() o.crypto = NewAESCrypto(config.CryptoKey) From 7e5798569b48f70971d3abf0ce92bbd109bcb61b Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Mon, 6 Mar 2023 04:13:35 -0800 Subject: [PATCH 187/502] fix: glob support for RedirectURIs Fixes #293 --- example/server/storage/client.go | 18 +++++++++++---- pkg/op/auth_request.go | 38 +++++++++++++++++++++++--------- pkg/op/client.go | 10 +++++++++ pkg/op/session.go | 12 ++++++++++ 4 files changed, 64 insertions(+), 14 deletions(-) diff --git a/example/server/storage/client.go b/example/server/storage/client.go index 0f3a703..0b98679 100644 --- a/example/server/storage/client.go +++ b/example/server/storage/client.go @@ -44,11 +44,21 @@ func (c *Client) RedirectURIs() []string { return c.redirectURIs } +// RedirectURIGlobs provide wildcarding for additional valid redirects +func (c *Client) RedirectURIGlobs() []string { + return nil +} + // PostLogoutRedirectURIs must return the registered post_logout_redirect_uris for sign-outs func (c *Client) PostLogoutRedirectURIs() []string { return []string{} } +// PostLogoutRedirectURIGlobs provide extra wildcarding for additional valid redirects +func (c *Client) PostLogoutRedirectURIGlobs() []string { + return nil +} + // ApplicationType must return the type of the client (app, native, user agent) func (c *Client) ApplicationType() op.ApplicationType { return c.applicationType @@ -113,14 +123,14 @@ func (c *Client) IsScopeAllowed(scope string) bool { // IDTokenUserinfoClaimsAssertion allows specifying if claims of scope profile, email, phone and address are asserted into the id_token // even if an access token if issued which violates the OIDC Core spec -//(5.4. Requesting Claims using Scope Values: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) +// (5.4. Requesting Claims using Scope Values: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) // some clients though require that e.g. email is always in the id_token when requested even if an access_token is issued func (c *Client) IDTokenUserinfoClaimsAssertion() bool { return c.idTokenUserinfoClaimsAssertion } // ClockSkew enables clients to instruct the OP to apply a clock skew on the various times and expirations -//(subtract from issued_at, add to expiration, ...) +// (subtract from issued_at, add to expiration, ...) func (c *Client) ClockSkew() time.Duration { return c.clockSkew } @@ -141,7 +151,7 @@ func RegisterClients(registerClients ...*Client) { // user-defined redirectURIs may include: // - http://localhost without port specification (e.g. http://localhost/auth/callback) // - custom protocol (e.g. custom://auth/callback) -//(the examples will be used as default, if none is provided) +// (the examples will be used as default, if none is provided) func NativeClient(id string, redirectURIs ...string) *Client { if len(redirectURIs) == 0 { redirectURIs = []string{ @@ -168,7 +178,7 @@ func NativeClient(id string, redirectURIs ...string) *Client { // WebClient will create a client of type web, which will always use Basic Auth and allow the use of refresh tokens // user-defined redirectURIs may include: // - http://localhost with port specification (e.g. http://localhost:9999/auth/callback) -//(the example will be used as default, if none is provided) +// (the example will be used as default, if none is provided) func WebClient(id, secret string, redirectURIs ...string) *Client { if len(redirectURIs) == 0 { redirectURIs = []string{ diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index d8c960e..ecfde28 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -6,6 +6,7 @@ import ( "net" "net/http" "net/url" + "path" "strings" "time" @@ -274,6 +275,28 @@ func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) { return scopes, nil } +// checkURIAginstRedirects just checks aginst the valid redirect URIs and ignores +// other factors. +func checkURIAginstRedirects(client Client, uri string) error { + if str.Contains(client.RedirectURIs(), uri) { + return nil + } + if globClient, ok := client.(HasRedirectGlobs); ok { + for _, uriGlob := range globClient.RedirectURIGlobs() { + isMatch, err := path.Match(uriGlob, uri) + if err != nil { + return oidc.ErrServerError().WithParent(err) + } + if isMatch { + return nil + } + } + } + return oidc.ErrInvalidRequestRedirectURI(). + WithDescription("The requested redirect_uri is missing in the client configuration. " + + "If you have any questions, you may contact the administrator of the application.") +} + // ValidateAuthReqRedirectURI validates the passed redirect_uri and response_type to the registered uris and client type func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.ResponseType) error { if uri == "" { @@ -281,19 +304,13 @@ func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.Res "Please ensure it is added to the request. If you have any questions, you may contact the administrator of the application.") } if strings.HasPrefix(uri, "https://") { - if !str.Contains(client.RedirectURIs(), uri) { - return oidc.ErrInvalidRequestRedirectURI(). - WithDescription("The requested redirect_uri is missing in the client configuration. " + - "If you have any questions, you may contact the administrator of the application.") - } - return nil + return checkURIAginstRedirects(client, uri) } if client.ApplicationType() == ApplicationTypeNative { return validateAuthReqRedirectURINative(client, uri, responseType) } - if !str.Contains(client.RedirectURIs(), uri) { - return oidc.ErrInvalidRequestRedirectURI().WithDescription("The requested redirect_uri is missing in the client configuration. " + - "If you have any questions, you may contact the administrator of the application.") + if err := checkURIAginstRedirects(client, uri); err != nil { + return err } if strings.HasPrefix(uri, "http://") { if client.DevMode() { @@ -313,10 +330,11 @@ func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.Res func validateAuthReqRedirectURINative(client Client, uri string, responseType oidc.ResponseType) error { parsedURL, isLoopback := HTTPLoopbackOrLocalhost(uri) isCustomSchema := !strings.HasPrefix(uri, "http://") - if str.Contains(client.RedirectURIs(), uri) { + if err := checkURIAginstRedirects(client, uri); err == nil { if client.DevMode() { return nil } + // The RedirectURIs are only valid for native clients when localhost or non-"http://" if isLoopback || isCustomSchema { return nil } diff --git a/pkg/op/client.go b/pkg/op/client.go index d9f7ab0..db3d69b 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -45,6 +45,16 @@ type Client interface { ClockSkew() time.Duration } +// HasRedirectGlobs is an optional interface that can be implemented by implementors of +// Client. See https://pkg.go.dev/path#Match for glob +// interpretation. Redirect URIs that match either the non-glob version or the +// glob version will be accepted. Glob URIs are only partially supported for native +// clients: "http://" is not allowed except for loopback or in dev mode. +type HasRedirectGlobs interface { + RedirectURIGlobs() []string + PostLogoutRedirectURIGlobs() []string +} + func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseType) bool { for _, t := range types { if t == responseType { diff --git a/pkg/op/session.go b/pkg/op/session.go index c4984fc..737bb86 100644 --- a/pkg/op/session.go +++ b/pkg/op/session.go @@ -4,6 +4,7 @@ import ( "context" "net/http" "net/url" + "path" httphelper "github.com/zitadel/oidc/pkg/http" "github.com/zitadel/oidc/pkg/oidc" @@ -98,5 +99,16 @@ func ValidateEndSessionPostLogoutRedirectURI(postLogoutRedirectURI string, clien return nil } } + if globClient, ok := client.(HasRedirectGlobs); ok { + for _, uriGlob := range globClient.PostLogoutRedirectURIGlobs() { + isMatch, err := path.Match(uriGlob, postLogoutRedirectURI) + if err != nil { + return oidc.ErrServerError().WithParent(err) + } + if isMatch { + return nil + } + } + } return oidc.ErrInvalidRequest().WithDescription("post_logout_redirect_uri invalid") } From fba465dc839d662dcd417b0d2efbc01831d261dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 14:31:00 +0200 Subject: [PATCH 188/502] chore(deps): bump github.com/stretchr/testify from 1.8.1 to 1.8.2 (#290) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.1 to 1.8.2. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.8.1...v1.8.2) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7c40e39..d8b4465 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/rs/cors v1.8.3 github.com/sirupsen/logrus v1.9.0 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.2 github.com/zitadel/logging v0.3.4 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 golang.org/x/text v0.7.0 diff --git a/go.sum b/go.sum index d5e856f..6ee11f2 100644 --- a/go.sum +++ b/go.sum @@ -142,8 +142,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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= From 62f2df7fa3483e7ea81f7dbcb29239bd4f13c70a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 14:34:12 +0200 Subject: [PATCH 189/502] chore(deps): bump actions/add-to-project from 0.4.0 to 0.4.1 (#294) Bumps [actions/add-to-project](https://github.com/actions/add-to-project) from 0.4.0 to 0.4.1. - [Release notes](https://github.com/actions/add-to-project/releases) - [Commits](https://github.com/actions/add-to-project/compare/v0.4.0...v0.4.1) --- updated-dependencies: - dependency-name: actions/add-to-project dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/issue.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index d7a3cc9..8671820 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -10,7 +10,7 @@ jobs: name: Add issue to project runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@v0.4.0 + - uses: actions/add-to-project@v0.4.1 with: # You can target a repository in a different organization # to the issue From 4bd2b742f909beb57e226ec7108b61338ecbe2f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 8 Mar 2023 11:43:47 +0200 Subject: [PATCH 190/502] chore: remove unused context in NewOpenIDProvider BREAKING CHANGE: - op.NewOpenIDProvider - op.NewDynamicOpenIDProvider The call chain of above functions did not use the context anywhere. This change removes the context from those fucntion arguments. --- example/server/dynamic/op.go | 2 +- example/server/exampleop/op.go | 9 ++++----- example/server/main.go | 6 +----- pkg/client/integration_test.go | 7 ++----- pkg/op/device_test.go | 2 +- pkg/op/op.go | 10 +++++----- 6 files changed, 14 insertions(+), 22 deletions(-) diff --git a/example/server/dynamic/op.go b/example/server/dynamic/op.go index 02c12b2..783c75c 100644 --- a/example/server/dynamic/op.go +++ b/example/server/dynamic/op.go @@ -125,7 +125,7 @@ func newDynamicOP(ctx context.Context, storage op.Storage, key [32]byte) (*op.Pr //this example has only static texts (in English), so we'll set the here accordingly SupportedUILocales: []language.Tag{language.English}, } - handler, err := op.NewDynamicOpenIDProvider(ctx, "/", config, storage, + handler, err := op.NewDynamicOpenIDProvider("/", config, storage, //we must explicitly allow the use of the http issuer op.WithAllowInsecure(), //as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth diff --git a/example/server/exampleop/op.go b/example/server/exampleop/op.go index b46be7f..5604483 100644 --- a/example/server/exampleop/op.go +++ b/example/server/exampleop/op.go @@ -1,7 +1,6 @@ package exampleop import ( - "context" "crypto/sha256" "log" "net/http" @@ -35,7 +34,7 @@ type Storage interface { // SetupServer creates an OIDC server with Issuer=http://localhost: // // Use one of the pre-made clients in storage/clients.go or register a new one. -func SetupServer(ctx context.Context, issuer string, storage Storage) *mux.Router { +func SetupServer(issuer string, storage Storage) *mux.Router { // the OpenID Provider requires a 32-byte key for (token) encryption // be sure to create a proper crypto random key and manage it securely! key := sha256.Sum256([]byte("test")) @@ -51,7 +50,7 @@ func SetupServer(ctx context.Context, issuer string, storage Storage) *mux.Route }) // creation of the OpenIDProvider with the just created in-memory Storage - provider, err := newOP(ctx, storage, issuer, key) + provider, err := newOP(storage, issuer, key) if err != nil { log.Fatal(err) } @@ -80,7 +79,7 @@ func SetupServer(ctx context.Context, issuer string, storage Storage) *mux.Route // newOP will create an OpenID Provider for localhost on a specified port with a given encryption key // and a predefined default logout uri // it will enable all options (see descriptions) -func newOP(ctx context.Context, storage op.Storage, issuer string, key [32]byte) (op.OpenIDProvider, error) { +func newOP(storage op.Storage, issuer string, key [32]byte) (op.OpenIDProvider, error) { config := &op.Config{ CryptoKey: key, @@ -112,7 +111,7 @@ func newOP(ctx context.Context, storage op.Storage, issuer string, key [32]byte) UserCode: op.UserCodeBase20, }, } - handler, err := op.NewOpenIDProvider(ctx, issuer, config, storage, + handler, err := op.NewOpenIDProvider(issuer, config, storage, //we must explicitly allow the use of the http issuer op.WithAllowInsecure(), // as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth diff --git a/example/server/main.go b/example/server/main.go index 6b40305..a2836ea 100644 --- a/example/server/main.go +++ b/example/server/main.go @@ -1,7 +1,6 @@ package main import ( - "context" "fmt" "log" "net/http" @@ -11,8 +10,6 @@ import ( ) func main() { - ctx := context.Background() - //we will run on :9998 port := "9998" //which gives us the issuer: http://localhost:9998/ @@ -23,7 +20,7 @@ func main() { // in this example it will be handled in-memory storage := storage.NewStorage(storage.NewUserStore(issuer)) - router := exampleop.SetupServer(ctx, issuer, storage) + router := exampleop.SetupServer(issuer, storage) server := &http.Server{ Addr: ":" + port, @@ -35,5 +32,4 @@ func main() { if err != nil { log.Fatal(err) } - <-ctx.Done() } diff --git a/pkg/client/integration_test.go b/pkg/client/integration_test.go index e89004a..f112b30 100644 --- a/pkg/client/integration_test.go +++ b/pkg/client/integration_test.go @@ -2,7 +2,6 @@ package client_test import ( "bytes" - "context" "io" "io/ioutil" "math/rand" @@ -30,14 +29,13 @@ import ( func TestRelyingPartySession(t *testing.T) { t.Log("------- start example OP ------") - ctx := context.Background() targetURL := "http://local-site" exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL)) var dh deferredHandler opServer := httptest.NewServer(&dh) defer opServer.Close() t.Logf("auth server at %s", opServer.URL) - dh.Handler = exampleop.SetupServer(ctx, opServer.URL, exampleStorage) + dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage) seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano())) clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25) @@ -79,14 +77,13 @@ func TestRelyingPartySession(t *testing.T) { func TestResourceServerTokenExchange(t *testing.T) { t.Log("------- start example OP ------") - ctx := context.Background() targetURL := "http://local-site" exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL)) var dh deferredHandler opServer := httptest.NewServer(&dh) defer opServer.Close() t.Logf("auth server at %s", opServer.URL) - dh.Handler = exampleop.SetupServer(ctx, opServer.URL, exampleStorage) + dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage) seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano())) clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25) diff --git a/pkg/op/device_test.go b/pkg/op/device_test.go index b3ac89d..de16a59 100644 --- a/pkg/op/device_test.go +++ b/pkg/op/device_test.go @@ -54,7 +54,7 @@ func init() { ) var err error - testProvider, err = op.NewOpenIDProvider(context.TODO(), testIssuer, config, + testProvider, err = op.NewOpenIDProvider(testIssuer, config, storage.NewStorage(storage.NewUserStore(testIssuer)), op.WithAllowInsecure(), ) if err != nil { diff --git a/pkg/op/op.go b/pkg/op/op.go index bb45425..ecb753e 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -157,15 +157,15 @@ type endpoints struct { // Successful logins should mark the request as authorized and redirect back to to // op.AuthCallbackURL(provider) which is probably /callback. On the redirect back // to the AuthCallbackURL, the request id should be passed as the "id" parameter. -func NewOpenIDProvider(ctx context.Context, issuer string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) { - return newProvider(ctx, config, storage, StaticIssuer(issuer), opOpts...) +func NewOpenIDProvider(issuer string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) { + return newProvider(config, storage, StaticIssuer(issuer), opOpts...) } -func NewDynamicOpenIDProvider(ctx context.Context, path string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) { - return newProvider(ctx, config, storage, IssuerFromHost(path), opOpts...) +func NewDynamicOpenIDProvider(path string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) { + return newProvider(config, storage, IssuerFromHost(path), opOpts...) } -func newProvider(ctx context.Context, config *Config, storage Storage, issuer func(bool) (IssuerFromRequest, error), opOpts ...Option) (_ *Provider, err error) { +func newProvider(config *Config, storage Storage, issuer func(bool) (IssuerFromRequest, error), opOpts ...Option) (_ *Provider, err error) { o := &Provider{ config: config, storage: storage, From eea2ed1a519c877792a959584f49e24f6a803d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 10 Mar 2023 09:46:25 +0200 Subject: [PATCH 191/502] fix: unmarshalling of scopes in access token (#320) The Scopes field in accessTokenClaims should be a SpaceDelimitedArray, in order to allow for correct unmarshalling. Fixes #318 --- pkg/oidc/token.go | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index 784684d..fb87e13 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -69,23 +69,23 @@ func NewAccessTokenClaims(issuer, subject string, audience []string, expiration } type accessTokenClaims struct { - Issuer string `json:"iss,omitempty"` - Subject string `json:"sub,omitempty"` - Audience Audience `json:"aud,omitempty"` - Expiration Time `json:"exp,omitempty"` - IssuedAt Time `json:"iat,omitempty"` - NotBefore Time `json:"nbf,omitempty"` - JWTID string `json:"jti,omitempty"` - AuthorizedParty string `json:"azp,omitempty"` - Nonce string `json:"nonce,omitempty"` - AuthTime Time `json:"auth_time,omitempty"` - CodeHash string `json:"c_hash,omitempty"` - AuthenticationContextClassReference string `json:"acr,omitempty"` - AuthenticationMethodsReferences []string `json:"amr,omitempty"` - SessionID string `json:"sid,omitempty"` - Scopes []string `json:"scope,omitempty"` - ClientID string `json:"client_id,omitempty"` - AccessTokenUseNumber int `json:"at_use_nbr,omitempty"` + Issuer string `json:"iss,omitempty"` + Subject string `json:"sub,omitempty"` + Audience Audience `json:"aud,omitempty"` + Expiration Time `json:"exp,omitempty"` + IssuedAt Time `json:"iat,omitempty"` + NotBefore Time `json:"nbf,omitempty"` + JWTID string `json:"jti,omitempty"` + AuthorizedParty string `json:"azp,omitempty"` + Nonce string `json:"nonce,omitempty"` + AuthTime Time `json:"auth_time,omitempty"` + CodeHash string `json:"c_hash,omitempty"` + AuthenticationContextClassReference string `json:"acr,omitempty"` + AuthenticationMethodsReferences []string `json:"amr,omitempty"` + SessionID string `json:"sid,omitempty"` + Scopes SpaceDelimitedArray `json:"scope,omitempty"` + ClientID string `json:"client_id,omitempty"` + AccessTokenUseNumber int `json:"at_use_nbr,omitempty"` claims map[string]interface{} `json:"-"` signatureAlg jose.SignatureAlgorithm `json:"-"` From dea8bc96eaf52e4d9caf5d42620ec5476635a17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 10 Mar 2023 16:31:22 +0200 Subject: [PATCH 192/502] refactor: use struct types for claim related types (#283) * oidc: add regression tests for token claim json this helps to verify that the same JSON is produced, after these types are refactored. * refactor: use struct types for claim related types BREAKING CHANGE: The following types are changed from interface to struct type: - AccessTokenClaims - IDTokenClaims - IntrospectionResponse - UserInfo and related types. The following methods of OPStorage now take a pointer to a struct type, instead of an interface: - SetUserinfoFromScopes - SetUserinfoFromToken - SetIntrospectionFromToken The following functions are now generic, so that type-safe extension of Claims is now possible: - op.VerifyIDTokenHint - op.VerifyAccessToken - rp.VerifyTokens - rp.VerifyIDToken - Changed UserInfoAddress to pointer in UserInfo and IntrospectionResponse. This was needed to make omitempty work correctly. - Copy or merge maps in IntrospectionResponse and SetUserInfo * op: add example for VerifyAccessToken * fix: rp: wrong assignment in WithIssuedAtMaxAge WithIssuedAtMaxAge assigned its value to v.maxAge, which was wrong. This change fixes that by assiging the duration to v.maxAgeIAT. * rp: add VerifyTokens example * oidc: add standard references to: - IDTokenClaims - IntrospectionResponse - UserInfo * only count coverage for `./pkg/...` --- .github/workflows/release.yml | 4 +- README.md | 4 +- example/client/api/api.go | 2 +- example/client/app/app.go | 2 +- example/client/github/github.go | 3 +- example/server/storage/storage.go | 37 +- example/server/storage/storage_dynamic.go | 6 +- go.mod | 22 +- go.sum | 11 +- internal/testutil/gen/gen.go | 58 ++ internal/testutil/token.go | 146 +++++ pkg/client/client.go | 4 +- pkg/client/integration_test.go | 9 +- pkg/client/rp/cli/cli.go | 6 +- pkg/client/rp/mock/generate.go | 3 - pkg/client/rp/mock/verifier.mock.go | 163 ----- pkg/client/rp/relying_party.go | 26 +- pkg/client/rp/verifier.go | 48 +- pkg/client/rp/verifier_test.go | 339 ++++++++++ pkg/client/rp/verifier_tokens_example_test.go | 86 +++ pkg/client/rs/resource_server.go | 4 +- pkg/oidc/introspection.go | 384 ++---------- pkg/oidc/introspection_test.go | 78 +++ pkg/oidc/regression_assert_test.go | 50 ++ pkg/oidc/regression_create_test.go | 24 + .../oidc.AccessTokenClaims.json | 26 + .../regression_data/oidc.IDTokenClaims.json | 51 ++ .../oidc.IntrospectionResponse.json | 44 ++ .../oidc.JWTProfileAssertionClaims.json | 11 + pkg/oidc/regression_data/oidc.UserInfo.json | 30 + pkg/oidc/regression_test.go | 40 ++ pkg/oidc/token.go | 578 +++++------------- pkg/oidc/token_request.go | 4 +- pkg/oidc/token_test.go | 227 +++++++ pkg/oidc/types.go | 55 +- pkg/oidc/types_test.go | 112 ++++ pkg/oidc/userinfo.go | 405 ++---------- pkg/oidc/userinfo_test.go | 111 ++-- pkg/oidc/util.go | 49 ++ pkg/oidc/util_test.go | 147 +++++ pkg/oidc/verifier.go | 6 + pkg/op/auth_request.go | 2 +- pkg/op/mock/storage.mock.go | 6 +- pkg/op/session.go | 2 +- pkg/op/storage.go | 8 +- pkg/op/token.go | 14 +- pkg/op/token_exchange.go | 14 +- pkg/op/token_intospection.go | 4 +- pkg/op/token_revocation.go | 4 +- pkg/op/userinfo.go | 6 +- pkg/op/verifier_access_token.go | 20 +- pkg/op/verifier_access_token_example_test.go | 70 +++ pkg/op/verifier_access_token_test.go | 126 ++++ pkg/op/verifier_id_token_hint.go | 22 +- pkg/op/verifier_id_token_hint_test.go | 161 +++++ 55 files changed, 2358 insertions(+), 1516 deletions(-) create mode 100644 internal/testutil/gen/gen.go create mode 100644 internal/testutil/token.go delete mode 100644 pkg/client/rp/mock/generate.go delete mode 100644 pkg/client/rp/mock/verifier.mock.go create mode 100644 pkg/client/rp/verifier_test.go create mode 100644 pkg/client/rp/verifier_tokens_example_test.go create mode 100644 pkg/oidc/introspection_test.go create mode 100644 pkg/oidc/regression_assert_test.go create mode 100644 pkg/oidc/regression_create_test.go create mode 100644 pkg/oidc/regression_data/oidc.AccessTokenClaims.json create mode 100644 pkg/oidc/regression_data/oidc.IDTokenClaims.json create mode 100644 pkg/oidc/regression_data/oidc.IntrospectionResponse.json create mode 100644 pkg/oidc/regression_data/oidc.JWTProfileAssertionClaims.json create mode 100644 pkg/oidc/regression_data/oidc.UserInfo.json create mode 100644 pkg/oidc/regression_test.go create mode 100644 pkg/oidc/token_test.go create mode 100644 pkg/oidc/util.go create mode 100644 pkg/oidc/util_test.go create mode 100644 pkg/op/verifier_access_token_example_test.go create mode 100644 pkg/op/verifier_access_token_test.go create mode 100644 pkg/op/verifier_id_token_hint_test.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9509826..2abef36 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - go: ['1.16', '1.17', '1.18', '1.19', '1.20'] + go: ['1.18', '1.19', '1.20'] name: Go ${{ matrix.go }} test steps: - uses: actions/checkout@v3 @@ -24,7 +24,7 @@ jobs: uses: actions/setup-go@v3 with: go-version: ${{ matrix.go }} - - run: go test -race -v -coverprofile=profile.cov -coverpkg=github.com/zitadel/oidc/... ./pkg/... + - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - uses: codecov/codecov-action@v3.1.1 with: file: ./profile.cov diff --git a/README.md b/README.md index 31287e9..7b9bf22 100644 --- a/README.md +++ b/README.md @@ -98,9 +98,7 @@ Versions that also build are marked with :warning:. | Version | Supported | |---------|--------------------| -| <1.16 | :x: | -| 1.16 | :warning: | -| 1.17 | :warning: | +| <1.18 | :x: | | 1.18 | :warning: | | 1.19 | :white_check_mark: | | 1.20 | :white_check_mark: | diff --git a/example/client/api/api.go b/example/client/api/api.go index c475354..8093b63 100644 --- a/example/client/api/api.go +++ b/example/client/api/api.go @@ -76,7 +76,7 @@ func main() { params := mux.Vars(r) requestedClaim := params["claim"] requestedValue := params["value"] - value, ok := resp.GetClaim(requestedClaim).(string) + value, ok := resp.Claims[requestedClaim].(string) if !ok || value == "" || value != requestedValue { http.Error(w, "claim does not match", http.StatusForbidden) return diff --git a/example/client/app/app.go b/example/client/app/app.go index 97e8948..560ac02 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -60,7 +60,7 @@ func main() { http.Handle("/login", rp.AuthURLHandler(state, provider)) // for demonstration purposes the returned userinfo response is written as JSON object onto response - marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) { + marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) { data, err := json.Marshal(info) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/example/client/github/github.go b/example/client/github/github.go index 57bb3ae..9cb813c 100644 --- a/example/client/github/github.go +++ b/example/client/github/github.go @@ -13,6 +13,7 @@ import ( "github.com/zitadel/oidc/v2/pkg/client/rp" "github.com/zitadel/oidc/v2/pkg/client/rp/cli" "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v2/pkg/oidc" ) var ( @@ -43,7 +44,7 @@ func main() { state := func() string { return uuid.New().String() } - token := cli.CodeFlow(ctx, relyingParty, callbackPath, port, state) + token := cli.CodeFlow[*oidc.IDTokenClaims](ctx, relyingParty, callbackPath, port, state) client := github.NewClient(relyingParty.OAuthConfig().Client(ctx, token.Token)) diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index 2794783..ff7889e 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -429,13 +429,13 @@ func (s *Storage) AuthorizeClientIDSecret(ctx context.Context, clientID, clientS // SetUserinfoFromScopes implements the op.Storage interface // it will be called for the creation of an id_token, so we'll just pass it to the private function without any further check -func (s *Storage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error { +func (s *Storage) SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error { return s.setUserinfo(ctx, userinfo, userID, clientID, scopes) } // SetUserinfoFromToken implements the op.Storage interface // it will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function -func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error { +func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, subject, origin string) error { token, ok := func() (*Token, bool) { s.lock.Lock() defer s.lock.Unlock() @@ -463,7 +463,7 @@ func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserIn // SetIntrospectionFromToken implements the op.Storage interface // it will be called for the introspection endpoint, so we read the token and pass the information from that to the private function -func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection oidc.IntrospectionResponse, tokenID, subject, clientID string) error { +func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) error { token, ok := func() (*Token, bool) { s.lock.Lock() defer s.lock.Unlock() @@ -480,14 +480,17 @@ func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection o // this will automatically be done by the library if you don't return an error // you can also return further information about the user / associated token // e.g. the userinfo (equivalent to userinfo endpoint) - err := s.setUserinfo(ctx, introspection, subject, clientID, token.Scopes) + + userInfo := new(oidc.UserInfo) + err := s.setUserinfo(ctx, userInfo, subject, clientID, token.Scopes) if err != nil { return err } + introspection.SetUserInfo(userInfo) //...and also the requested scopes... - introspection.SetScopes(token.Scopes) + introspection.Scope = token.Scopes //...and the client the token was issued to - introspection.SetClientID(token.ApplicationID) + introspection.ClientID = token.ApplicationID return nil } } @@ -608,7 +611,7 @@ func (s *Storage) accessToken(applicationID, refreshTokenID, subject string, aud } // setUserinfo sets the info based on the user, scopes and if necessary the clientID -func (s *Storage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSetter, userID, clientID string, scopes []string) (err error) { +func (s *Storage) setUserinfo(ctx context.Context, userInfo *oidc.UserInfo, userID, clientID string, scopes []string) (err error) { s.lock.Lock() defer s.lock.Unlock() user := s.userStore.GetUserByID(userID) @@ -618,17 +621,19 @@ func (s *Storage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSetter, for _, scope := range scopes { switch scope { case oidc.ScopeOpenID: - userInfo.SetSubject(user.ID) + userInfo.Subject = user.ID case oidc.ScopeEmail: - userInfo.SetEmail(user.Email, user.EmailVerified) + userInfo.Email = user.Email + userInfo.EmailVerified = oidc.Bool(user.EmailVerified) case oidc.ScopeProfile: - userInfo.SetPreferredUsername(user.Username) - userInfo.SetName(user.FirstName + " " + user.LastName) - userInfo.SetFamilyName(user.LastName) - userInfo.SetGivenName(user.FirstName) - userInfo.SetLocale(user.PreferredLanguage) + userInfo.PreferredUsername = user.Username + userInfo.Name = user.FirstName + " " + user.LastName + userInfo.FamilyName = user.LastName + userInfo.GivenName = user.FirstName + userInfo.Locale = oidc.NewLocale(user.PreferredLanguage) case oidc.ScopePhone: - userInfo.SetPhone(user.Phone, user.PhoneVerified) + userInfo.PhoneNumber = user.Phone + userInfo.PhoneNumberVerified = user.PhoneVerified case CustomScope: // you can also have a custom scope and assert public or custom claims based on that userInfo.AppendClaims(CustomClaim, customClaim(clientID)) @@ -698,7 +703,7 @@ func (s *Storage) GetPrivateClaimsFromTokenExchangeRequest(ctx context.Context, // SetUserinfoFromScopesForTokenExchange implements the op.TokenExchangeStorage interface // it will be called for the creation of an id_token - we are using the same private function as for other flows, // plus adding token exchange specific claims related to delegation or impersonation -func (s *Storage) SetUserinfoFromTokenExchangeRequest(ctx context.Context, userinfo oidc.UserInfoSetter, request op.TokenExchangeRequest) error { +func (s *Storage) SetUserinfoFromTokenExchangeRequest(ctx context.Context, userinfo *oidc.UserInfo, request op.TokenExchangeRequest) error { err := s.setUserinfo(ctx, userinfo, request.GetSubject(), request.GetClientID(), request.GetScopes()) if err != nil { return err diff --git a/example/server/storage/storage_dynamic.go b/example/server/storage/storage_dynamic.go index d424a89..6e5ee32 100644 --- a/example/server/storage/storage_dynamic.go +++ b/example/server/storage/storage_dynamic.go @@ -198,7 +198,7 @@ func (s *multiStorage) AuthorizeClientIDSecret(ctx context.Context, clientID, cl // SetUserinfoFromScopes implements the op.Storage interface // it will be called for the creation of an id_token, so we'll just pass it to the private function without any further check -func (s *multiStorage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error { +func (s *multiStorage) SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error { storage, err := s.storageFromContext(ctx) if err != nil { return err @@ -208,7 +208,7 @@ func (s *multiStorage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc. // SetUserinfoFromToken implements the op.Storage interface // it will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function -func (s *multiStorage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error { +func (s *multiStorage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, subject, origin string) error { storage, err := s.storageFromContext(ctx) if err != nil { return err @@ -218,7 +218,7 @@ func (s *multiStorage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.U // SetIntrospectionFromToken implements the op.Storage interface // it will be called for the introspection endpoint, so we read the token and pass the information from that to the private function -func (s *multiStorage) SetIntrospectionFromToken(ctx context.Context, introspection oidc.IntrospectionResponse, tokenID, subject, clientID string) error { +func (s *multiStorage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) error { storage, err := s.storageFromContext(ctx) if err != nil { return err diff --git a/go.mod b/go.mod index 2691e57..9ed1e8e 100644 --- a/go.mod +++ b/go.mod @@ -1,22 +1,36 @@ module github.com/zitadel/oidc/v2 -go 1.16 +go 1.18 require ( github.com/golang/mock v1.6.0 - github.com/google/go-cmp v0.5.2 // indirect github.com/google/go-github/v31 v31.0.0 github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 github.com/gorilla/schema v1.2.0 github.com/gorilla/securecookie v1.1.1 github.com/jeremija/gosubmit v0.2.7 - github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/muhlemmer/gu v0.3.0 github.com/rs/cors v1.8.3 github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.1 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 golang.org/x/text v0.6.0 - gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/square/go-jose.v2 v2.6.0 ) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang/protobuf v1.4.2 // indirect + github.com/google/go-cmp v0.5.2 // indirect + github.com/google/go-querystring v1.0.0 // indirect + github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect + golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect + google.golang.org/appengine v1.6.6 // indirect + google.golang.org/protobuf v1.25.0 // indirect + gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index c73eb9d..1933228 100644 --- a/go.sum +++ b/go.sum @@ -123,6 +123,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/muhlemmer/gu v0.3.0 h1:UwNv9xXGp1WDgHKgk7ljjh3duh1w4ZAY1k1NsWBYl3Y= +github.com/muhlemmer/gu v0.3.0/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -146,7 +148,6 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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= @@ -190,7 +191,6 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -217,7 +217,6 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -237,7 +236,6 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -266,19 +264,15 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -325,7 +319,6 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/testutil/gen/gen.go b/internal/testutil/gen/gen.go new file mode 100644 index 0000000..a9f5925 --- /dev/null +++ b/internal/testutil/gen/gen.go @@ -0,0 +1,58 @@ +// Package gen allows generating of example tokens and claims. +// +// go run ./internal/testutil/gen +package main + +import ( + "encoding/json" + "fmt" + "os" + + tu "github.com/zitadel/oidc/v2/internal/testutil" + "github.com/zitadel/oidc/v2/pkg/oidc" +) + +var custom = map[string]any{ + "foo": "Hello, World!", + "bar": struct { + Count int `json:"count,omitempty"` + Tags []string `json:"tags,omitempty"` + }{ + Count: 22, + Tags: []string{"some", "tags"}, + }, +} + +func main() { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + + accessToken, atClaims := tu.NewAccessTokenCustom( + tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration.AddDate(99, 0, 0), tu.ValidJWTID, + tu.ValidClientID, tu.ValidSkew, custom, + ) + atHash, err := oidc.ClaimHash(accessToken, tu.SignatureAlgorithm) + if err != nil { + panic(err) + } + + idToken, idClaims := tu.NewIDTokenCustom( + tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration.AddDate(99, 0, 0), tu.ValidAuthTime, + tu.ValidNonce, tu.ValidACR, tu.ValidAMR, tu.ValidClientID, + tu.ValidSkew, atHash, custom, + ) + + fmt.Println("access token claims:") + if err := enc.Encode(atClaims); err != nil { + panic(err) + } + fmt.Printf("access token:\n%s\n", accessToken) + + fmt.Println("ID token claims:") + if err := enc.Encode(idClaims); err != nil { + panic(err) + } + fmt.Printf("ID token:\n%s\n", idToken) +} diff --git a/internal/testutil/token.go b/internal/testutil/token.go new file mode 100644 index 0000000..121aa0b --- /dev/null +++ b/internal/testutil/token.go @@ -0,0 +1,146 @@ +// Package testuril helps setting up required data for testing, +// such as tokens, claims and verifiers. +package testutil + +import ( + "context" + "encoding/json" + "errors" + "time" + + "github.com/zitadel/oidc/v2/pkg/oidc" + "gopkg.in/square/go-jose.v2" +) + +// KeySet implements oidc.Keys +type KeySet struct{} + +// VerifySignature implments op.KeySet. +func (KeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) { + if ctx.Err() != nil { + return nil, err + } + + return jws.Verify(WebKey.Public()) +} + +// use a reproducible signing key +const webkeyJSON = `{"kty":"RSA","kid":"1","alg":"PS512","n":"x6JoG8t2Li68JSwPwnh51TvHYFf3z72tQ3wmJG3VosU6MdJF0gSTCIwflOJ38OWE6hYtN1WAeyBy2CYdnXd1QZzkK_apGK4M7hsNA9jCTg8NOZjLPL0ww1jp7313Skla7mbm90uNdg4TUNp2n_r-sCYywI-9cfSlhzLSksxKK_BRdzy6xW20daAcI-mErQXIcvdYIguunJk_uTb8kJedsWMcQ4Mb57QujUok2Z2YabWyb9Fi1_StixXJvd_WEu93SHNMORB0u6ymnO3aZJdATLdhtcP-qsVicQhffpqVazmZQPf7K-7n4I5vJE4g9XXzZ2dSKSp3Ewe_nna_2kvbCw","e":"AQAB","d":"sl3F_QeF2O-CxQegMRYpbL6Tfd47GM6VDxXOkn_cACmNvFPudB4ILPvdf830cjTv06Lq1WS8fcZZNgygK0A_cNc3-pvRK67e-KMMtuIlgU7rdwmwlN1Iw1Ee-w6z1ZjC-PzR4iQMCW28DmKS2I-OnV4TvH7xOe7nMmvTPrvujV__YKfUxvAWXJG7_wtaJBGplezn5nNsKG2Ot9h0mhMdYUgGC36wLxo3Q5d4m79EXQYdhm89EfxogwvMmHRes5PNpHRuDZRHGAI4RZi2KvgmqF07e1Qdq4TqbQnY5pCYrdjqvEFFjGC6jTE-ak_b21FcSVy-9aZHyf04U4g5-cIUEQ","p":"7AaicFryJCHRekdSkx8tfPxaSiyEuN8jhP9cLqs4rLkIbrSHmanPhjnLe-Tlh3icQ8hPoy6WC8ktLwsrzbfGIh4U_zgAfvtD1Y_lZM-YSWZsxqlrGiI5do11iVzzoy4a1XdkgOjHQz9y6J-uoA9jY8ILG7VaEZQnaYwWZV3cspk","q":"2Ide9hlwthXJQJYqI0mibM5BiGBxJ4CafPmF1DYNXggBCczZ6ERGReNTGM_AEhy5mvLXUH6uBSOJlfHTYzx49C1GgIO3hEWVEGAKAytVRL6RfAkVSOXMQUp-HjXKpGg_Nx1SJxQf3rulbW8HXO4KqIlloyIXpPQSK7jB8A4hJUM","dp":"1nmc6F4sRNsaQHRJO_mL21RxM4_KtzfFThjCCoJ6iLHHUNnpkp_1PTKNjrLMRFM8JHgErfMqU-FmlqYfEtvZRq1xRQ39nWX0GT-eIwJljuVtGQVglqnc77bRxJXbqz-9EJdik6VzVM92Op7IDxiMp1zvvSkJhInNWqL6wvgNEZk","dq":"dlHizlAwiw90ndpwxD-khhhfLwqkSpW31br0KnYu78cn6hcKrCVC0UXbTp-XsU4JDmbMyauvpBc7Q7iVbpDI94UWFXvkeF8diYkxb3HqclpAXasI-oC4EKWILTHvvc9JW_Clx7zzfV7Ekvws5dcd8-LAq1gh232TwFiBgY_3BMk","qi":"E1k_9W3odXgcmIP2PCJztE7hB7jeuAL1ElAY88VJBBPY670uwOEjKL2VfQuz9q9IjzLAvcgf7vS9blw2RHP_XqHqSOlJWGwvMQTF0Q8zLknCgKt8q7HQQNWIJcBZ8qdUVn02-qf4E3tgZ3JHaHNs8imA_L-__WoUmzC4z5jH_lM"}` + +const SignatureAlgorithm = jose.RS256 + +var ( + WebKey jose.JSONWebKey + Signer jose.Signer +) + +func init() { + err := json.Unmarshal([]byte(webkeyJSON), &WebKey) + if err != nil { + panic(err) + } + Signer, err = jose.NewSigner(jose.SigningKey{Algorithm: SignatureAlgorithm, Key: WebKey}, nil) + if err != nil { + panic(err) + } +} + +func signEncodeTokenClaims(claims any) string { + payload, err := json.Marshal(claims) + if err != nil { + panic(err) + } + object, err := Signer.Sign(payload) + if err != nil { + panic(err) + } + token, err := object.CompactSerialize() + if err != nil { + panic(err) + } + return token +} + +func claimsMap(claims any) map[string]any { + data, err := json.Marshal(claims) + if err != nil { + panic(err) + } + dst := make(map[string]any) + if err = json.Unmarshal(data, &dst); err != nil { + panic(err) + } + return dst +} + +func NewIDTokenCustom(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string, skew time.Duration, atHash string, custom map[string]any) (string, *oidc.IDTokenClaims) { + claims := oidc.NewIDTokenClaims(issuer, subject, audience, expiration, authTime, nonce, acr, amr, clientID, skew) + claims.AccessTokenHash = atHash + claims.Claims = custom + token := signEncodeTokenClaims(claims) + + // set this so that assertion in tests will work + claims.SignatureAlg = SignatureAlgorithm + claims.Claims = claimsMap(claims) + return token, claims +} + +// NewIDToken creates a new IDTokenClaims with passed data and returns a signed token and claims. +func NewIDToken(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string, skew time.Duration, atHash string) (string, *oidc.IDTokenClaims) { + return NewIDTokenCustom(issuer, subject, audience, expiration, authTime, nonce, acr, amr, clientID, skew, atHash, nil) +} + +func NewAccessTokenCustom(issuer, subject string, audience []string, expiration time.Time, jwtid, clientID string, skew time.Duration, custom map[string]any) (string, *oidc.AccessTokenClaims) { + claims := oidc.NewAccessTokenClaims(issuer, subject, audience, expiration, jwtid, clientID, skew) + claims.Claims = custom + token := signEncodeTokenClaims(claims) + + // set this so that assertion in tests will work + claims.SignatureAlg = SignatureAlgorithm + claims.Claims = claimsMap(claims) + return token, claims +} + +// NewAcccessToken creates a new AccessTokenClaims with passed data and returns a signed token and claims. +func NewAccessToken(issuer, subject string, audience []string, expiration time.Time, jwtid, clientID string, skew time.Duration) (string, *oidc.AccessTokenClaims) { + return NewAccessTokenCustom(issuer, subject, audience, expiration, jwtid, clientID, skew, nil) +} + +const InvalidSignatureToken = `eyJhbGciOiJQUzUxMiJ9.eyJpc3MiOiJsb2NhbC5jb20iLCJzdWIiOiJ0aW1AbG9jYWwuY29tIiwiYXVkIjpbInVuaXQiLCJ0ZXN0IiwiNTU1NjY2Il0sImV4cCI6MTY3Nzg0MDQzMSwiaWF0IjoxNjc3ODQwMzcwLCJhdXRoX3RpbWUiOjE2Nzc4NDAzMTAsIm5vbmNlIjoiMTIzNDUiLCJhY3IiOiJzb21ldGhpbmciLCJhbXIiOlsiZm9vIiwiYmFyIl0sImF6cCI6IjU1NTY2NiJ9.DtZmvVkuE4Hw48ijBMhRJbxEWCr_WEYuPQBMY73J9TP6MmfeNFkjVJf4nh4omjB9gVLnQ-xhEkNOe62FS5P0BB2VOxPuHZUj34dNspCgG3h98fGxyiMb5vlIYAHDF9T-w_LntlYItohv63MmdYR-hPpAqjXE7KOfErf-wUDGE9R3bfiQ4HpTdyFJB1nsToYrZ9lhP2mzjTCTs58ckZfQ28DFHn_lfHWpR4rJBgvLx7IH4rMrUayr09Ap-PxQLbv0lYMtmgG1z3JK8MXnuYR0UJdZnEIezOzUTlThhCXB-nvuAXYjYxZZTR0FtlgZUHhIpYK0V2abf_Q_Or36akNCUg` + +// These variables always result in a valid token +var ( + ValidIssuer = "local.com" + ValidSubject = "tim@local.com" + ValidAudience = []string{"unit", "test"} + ValidAuthTime = time.Now().Add(-time.Minute) // authtime is always 1 minute in the past + ValidExpiration = ValidAuthTime.Add(2 * time.Minute) // token is always 1 more minute available + ValidJWTID = "9876" + ValidNonce = "12345" + ValidACR = "something" + ValidAMR = []string{"foo", "bar"} + ValidClientID = "555666" + ValidSkew = time.Second +) + +// ValidIDToken returns a token and claims that are in the token. +// It uses the Valid* global variables and the token will always +// pass verification. +func ValidIDToken() (string, *oidc.IDTokenClaims) { + return NewIDToken(ValidIssuer, ValidSubject, ValidAudience, ValidExpiration, ValidAuthTime, ValidNonce, ValidACR, ValidAMR, ValidClientID, ValidSkew, "") +} + +// ValidAccessToken returns a token and claims that are in the token. +// It uses the Valid* global variables and the token always passes +// verification within the same test run. +func ValidAccessToken() (string, *oidc.AccessTokenClaims) { + return NewAccessToken(ValidIssuer, ValidSubject, ValidAudience, ValidExpiration, ValidJWTID, ValidClientID, ValidSkew) +} + +// ACRVerify is a oidc.ACRVerifier func. +func ACRVerify(acr string) error { + if acr != ValidACR { + return errors.New("invalid acr") + } + return nil +} diff --git a/pkg/client/client.go b/pkg/client/client.go index ebe1442..9eda973 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -176,8 +176,8 @@ func SignedJWTProfileAssertion(clientID string, audience []string, expiration ti Issuer: clientID, Subject: clientID, Audience: audience, - ExpiresAt: oidc.Time(exp), - IssuedAt: oidc.Time(iat), + ExpiresAt: oidc.FromTime(exp), + IssuedAt: oidc.FromTime(iat), }, signer) } diff --git a/pkg/client/integration_test.go b/pkg/client/integration_test.go index f112b30..7f6ca62 100644 --- a/pkg/client/integration_test.go +++ b/pkg/client/integration_test.go @@ -235,19 +235,19 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID, } var email string - redirect := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) { + redirect := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) { require.NotNil(t, tokens, "tokens") require.NotNil(t, info, "info") t.Log("access token", tokens.AccessToken) t.Log("refresh token", tokens.RefreshToken) t.Log("id token", tokens.IDToken) - t.Log("email", info.GetEmail()) + t.Log("email", info.Email) accessToken = tokens.AccessToken refreshToken = tokens.RefreshToken idToken = tokens.IDToken - email = info.GetEmail() - http.Redirect(w, r, targetURL, http.StatusFound) + email = info.Email + http.Redirect(w, r, targetURL, 302) } rp.CodeExchangeHandler(rp.UserinfoCallback(redirect), provider)(capturedW, get) @@ -258,7 +258,6 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID, } }() require.Less(t, capturedW.Code, 400, "token exchange response code") - require.Less(t, capturedW.Code, 400, "token exchange response code") //nolint:bodyclose resp = capturedW.Result() diff --git a/pkg/client/rp/cli/cli.go b/pkg/client/rp/cli/cli.go index 936f319..91b200d 100644 --- a/pkg/client/rp/cli/cli.go +++ b/pkg/client/rp/cli/cli.go @@ -13,13 +13,13 @@ const ( loginPath = "/login" ) -func CodeFlow(ctx context.Context, relyingParty rp.RelyingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens { +func CodeFlow[C oidc.IDClaims](ctx context.Context, relyingParty rp.RelyingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens[C] { codeflowCtx, codeflowCancel := context.WithCancel(ctx) defer codeflowCancel() - tokenChan := make(chan *oidc.Tokens, 1) + tokenChan := make(chan *oidc.Tokens[C], 1) - callback := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty) { + callback := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp rp.RelyingParty) { tokenChan <- tokens msg := "

Success!

" msg = msg + "

You are authenticated and can now return to the CLI.

" diff --git a/pkg/client/rp/mock/generate.go b/pkg/client/rp/mock/generate.go deleted file mode 100644 index 7db81ea..0000000 --- a/pkg/client/rp/mock/generate.go +++ /dev/null @@ -1,3 +0,0 @@ -package mock - -//go:generate mockgen -package mock -destination ./verifier.mock.go github.com/zitadel/oidc/v2/pkg/client/rp IDTokenVerifier diff --git a/pkg/client/rp/mock/verifier.mock.go b/pkg/client/rp/mock/verifier.mock.go deleted file mode 100644 index eac6a79..0000000 --- a/pkg/client/rp/mock/verifier.mock.go +++ /dev/null @@ -1,163 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/v2/pkg/client/rp (interfaces: IDTokenVerifier) - -// Package mock is a generated GoMock package. -package mock - -import ( - context "context" - reflect "reflect" - time "time" - - gomock "github.com/golang/mock/gomock" - oidc "github.com/zitadel/oidc/v2/pkg/oidc" -) - -// MockIDTokenVerifier is a mock of IDTokenVerifier interface. -type MockIDTokenVerifier struct { - ctrl *gomock.Controller - recorder *MockIDTokenVerifierMockRecorder -} - -// MockIDTokenVerifierMockRecorder is the mock recorder for MockIDTokenVerifier. -type MockIDTokenVerifierMockRecorder struct { - mock *MockIDTokenVerifier -} - -// NewMockIDTokenVerifier creates a new mock instance. -func NewMockIDTokenVerifier(ctrl *gomock.Controller) *MockIDTokenVerifier { - mock := &MockIDTokenVerifier{ctrl: ctrl} - mock.recorder = &MockIDTokenVerifierMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockIDTokenVerifier) EXPECT() *MockIDTokenVerifierMockRecorder { - return m.recorder -} - -// ACR mocks base method. -func (m *MockIDTokenVerifier) ACR() oidc.ACRVerifier { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ACR") - ret0, _ := ret[0].(oidc.ACRVerifier) - return ret0 -} - -// ACR indicates an expected call of ACR. -func (mr *MockIDTokenVerifierMockRecorder) ACR() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ACR", reflect.TypeOf((*MockIDTokenVerifier)(nil).ACR)) -} - -// ClientID mocks base method. -func (m *MockIDTokenVerifier) ClientID() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ClientID") - ret0, _ := ret[0].(string) - return ret0 -} - -// ClientID indicates an expected call of ClientID. -func (mr *MockIDTokenVerifierMockRecorder) ClientID() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientID", reflect.TypeOf((*MockIDTokenVerifier)(nil).ClientID)) -} - -// Issuer mocks base method. -func (m *MockIDTokenVerifier) Issuer() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Issuer") - ret0, _ := ret[0].(string) - return ret0 -} - -// Issuer indicates an expected call of Issuer. -func (mr *MockIDTokenVerifierMockRecorder) Issuer() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Issuer", reflect.TypeOf((*MockIDTokenVerifier)(nil).Issuer)) -} - -// KeySet mocks base method. -func (m *MockIDTokenVerifier) KeySet() oidc.KeySet { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KeySet") - ret0, _ := ret[0].(oidc.KeySet) - return ret0 -} - -// KeySet indicates an expected call of KeySet. -func (mr *MockIDTokenVerifierMockRecorder) KeySet() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeySet", reflect.TypeOf((*MockIDTokenVerifier)(nil).KeySet)) -} - -// MaxAge mocks base method. -func (m *MockIDTokenVerifier) MaxAge() time.Duration { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "MaxAge") - ret0, _ := ret[0].(time.Duration) - return ret0 -} - -// MaxAge indicates an expected call of MaxAge. -func (mr *MockIDTokenVerifierMockRecorder) MaxAge() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxAge", reflect.TypeOf((*MockIDTokenVerifier)(nil).MaxAge)) -} - -// MaxAgeIAT mocks base method. -func (m *MockIDTokenVerifier) MaxAgeIAT() time.Duration { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "MaxAgeIAT") - ret0, _ := ret[0].(time.Duration) - return ret0 -} - -// MaxAgeIAT indicates an expected call of MaxAgeIAT. -func (mr *MockIDTokenVerifierMockRecorder) MaxAgeIAT() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxAgeIAT", reflect.TypeOf((*MockIDTokenVerifier)(nil).MaxAgeIAT)) -} - -// Nonce mocks base method. -func (m *MockIDTokenVerifier) Nonce(arg0 context.Context) string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Nonce", arg0) - ret0, _ := ret[0].(string) - return ret0 -} - -// Nonce indicates an expected call of Nonce. -func (mr *MockIDTokenVerifierMockRecorder) Nonce(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Nonce", reflect.TypeOf((*MockIDTokenVerifier)(nil).Nonce), arg0) -} - -// Offset mocks base method. -func (m *MockIDTokenVerifier) Offset() time.Duration { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Offset") - ret0, _ := ret[0].(time.Duration) - return ret0 -} - -// Offset indicates an expected call of Offset. -func (mr *MockIDTokenVerifierMockRecorder) Offset() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Offset", reflect.TypeOf((*MockIDTokenVerifier)(nil).Offset)) -} - -// SupportedSignAlgs mocks base method. -func (m *MockIDTokenVerifier) SupportedSignAlgs() []string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SupportedSignAlgs") - ret0, _ := ret[0].([]string) - return ret0 -} - -// SupportedSignAlgs indicates an expected call of SupportedSignAlgs. -func (mr *MockIDTokenVerifierMockRecorder) SupportedSignAlgs() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportedSignAlgs", reflect.TypeOf((*MockIDTokenVerifier)(nil).SupportedSignAlgs)) -} diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 5bd3558..8aa7b1e 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -373,7 +373,7 @@ func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (stri // CodeExchange handles the oauth2 code exchange, extracting and validating the id_token // returning it parsed together with the oauth2 tokens (access, refresh) -func CodeExchange(ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) { +func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens[C], err error) { ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient()) codeOpts := make([]oauth2.AuthCodeOption, 0) for _, opt := range opts { @@ -386,7 +386,7 @@ func CodeExchange(ctx context.Context, code string, rp RelyingParty, opts ...Cod } if rp.IsOAuth2Only() { - return &oidc.Tokens{Token: token}, nil + return &oidc.Tokens[C]{Token: token}, nil } idTokenString, ok := token.Extra(idTokenKey).(string) @@ -394,20 +394,20 @@ func CodeExchange(ctx context.Context, code string, rp RelyingParty, opts ...Cod return nil, errors.New("id_token missing") } - idToken, err := VerifyTokens(ctx, token.AccessToken, idTokenString, rp.IDTokenVerifier()) + idToken, err := VerifyTokens[C](ctx, token.AccessToken, idTokenString, rp.IDTokenVerifier()) if err != nil { return nil, err } - return &oidc.Tokens{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil + return &oidc.Tokens[C]{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil } -type CodeExchangeCallback func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp RelyingParty) +type CodeExchangeCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) // CodeExchangeHandler extends the `CodeExchange` method with a http handler // including cookie handling for secure `state` transfer // and optional PKCE code verifier checking -func CodeExchangeHandler(callback CodeExchangeCallback, rp RelyingParty) http.HandlerFunc { +func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp RelyingParty) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { state, err := tryReadStateCookie(w, r, rp) if err != nil { @@ -436,7 +436,7 @@ func CodeExchangeHandler(callback CodeExchangeCallback, rp RelyingParty) http.Ha } codeOpts = append(codeOpts, WithClientAssertionJWT(assertion)) } - tokens, err := CodeExchange(r.Context(), params.Get("code"), rp, codeOpts...) + tokens, err := CodeExchange[C](r.Context(), params.Get("code"), rp, codeOpts...) if err != nil { http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) return @@ -445,13 +445,13 @@ func CodeExchangeHandler(callback CodeExchangeCallback, rp RelyingParty) http.Ha } } -type CodeExchangeUserinfoCallback func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, provider RelyingParty, info oidc.UserInfo) +type CodeExchangeUserinfoCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, provider RelyingParty, info *oidc.UserInfo) // UserinfoCallback wraps the callback function of the CodeExchangeHandler // and calls the userinfo endpoint with the access token // on success it will pass the userinfo into its callback function as well -func UserinfoCallback(f CodeExchangeUserinfoCallback) CodeExchangeCallback { - return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp RelyingParty) { +func UserinfoCallback[C oidc.IDClaims](f CodeExchangeUserinfoCallback[C]) CodeExchangeCallback[C] { + return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) { info, err := Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp) if err != nil { http.Error(w, "userinfo failed: "+err.Error(), http.StatusUnauthorized) @@ -462,17 +462,17 @@ func UserinfoCallback(f CodeExchangeUserinfoCallback) CodeExchangeCallback { } // Userinfo will call the OIDC Userinfo Endpoint with the provided token -func Userinfo(token, tokenType, subject string, rp RelyingParty) (oidc.UserInfo, error) { +func Userinfo(token, tokenType, subject string, rp RelyingParty) (*oidc.UserInfo, error) { req, err := http.NewRequest("GET", rp.UserinfoEndpoint(), nil) if err != nil { return nil, err } req.Header.Set("authorization", tokenType+" "+token) - userinfo := oidc.NewUserInfo() + userinfo := new(oidc.UserInfo) if err := httphelper.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil { return nil, err } - if userinfo.GetSubject() != subject { + if userinfo.Subject != subject { return nil, ErrUserInfoSubNotMatching } return userinfo, nil diff --git a/pkg/client/rp/verifier.go b/pkg/client/rp/verifier.go index f3db128..75d149b 100644 --- a/pkg/client/rp/verifier.go +++ b/pkg/client/rp/verifier.go @@ -21,69 +21,71 @@ type IDTokenVerifier interface { // VerifyTokens implement the Token Response Validation as defined in OIDC specification // https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation -func VerifyTokens(ctx context.Context, accessToken, idTokenString string, v IDTokenVerifier) (oidc.IDTokenClaims, error) { - idToken, err := VerifyIDToken(ctx, idTokenString, v) +func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken string, v IDTokenVerifier) (claims C, err error) { + var nilClaims C + + claims, err = VerifyIDToken[C](ctx, idToken, v) if err != nil { - return nil, err + return nilClaims, err } - if err := VerifyAccessToken(accessToken, idToken.GetAccessTokenHash(), idToken.GetSignatureAlgorithm()); err != nil { - return nil, err + if err := VerifyAccessToken(accessToken, claims.GetAccessTokenHash(), claims.GetSignatureAlgorithm()); err != nil { + return nilClaims, err } - return idToken, nil + return claims, nil } // VerifyIDToken validates the id token according to // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation -func VerifyIDToken(ctx context.Context, token string, v IDTokenVerifier) (oidc.IDTokenClaims, error) { - claims := oidc.EmptyIDTokenClaims() +func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v IDTokenVerifier) (claims C, err error) { + var nilClaims C decrypted, err := oidc.DecryptToken(token) if err != nil { - return nil, err + return nilClaims, err } - payload, err := oidc.ParseToken(decrypted, claims) + payload, err := oidc.ParseToken(decrypted, &claims) if err != nil { - return nil, err + return nilClaims, err } if err := oidc.CheckSubject(claims); err != nil { - return nil, err + return nilClaims, err } if err = oidc.CheckIssuer(claims, v.Issuer()); err != nil { - return nil, err + return nilClaims, err } if err = oidc.CheckAudience(claims, v.ClientID()); err != nil { - return nil, err + return nilClaims, err } if err = oidc.CheckAuthorizedParty(claims, v.ClientID()); err != nil { - return nil, err + return nilClaims, err } if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil { - return nil, err + return nilClaims, err } if err = oidc.CheckExpiration(claims, v.Offset()); err != nil { - return nil, err + return nilClaims, err } if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT(), v.Offset()); err != nil { - return nil, err + return nilClaims, err } if err = oidc.CheckNonce(claims, v.Nonce(ctx)); err != nil { - return nil, err + return nilClaims, err } if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR()); err != nil { - return nil, err + return nilClaims, err } if err = oidc.CheckAuthTime(claims, v.MaxAge()); err != nil { - return nil, err + return nilClaims, err } return claims, nil } @@ -112,7 +114,7 @@ func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ... issuer: issuer, clientID: clientID, keySet: keySet, - offset: 1 * time.Second, + offset: time.Second, nonce: func(_ context.Context) string { return "" }, @@ -139,7 +141,7 @@ func WithIssuedAtOffset(offset time.Duration) func(*idTokenVerifier) { // WithIssuedAtMaxAge provides the ability to define the maximum duration between iat and now func WithIssuedAtMaxAge(maxAge time.Duration) func(*idTokenVerifier) { return func(v *idTokenVerifier) { - v.maxAge = maxAge + v.maxAgeIAT = maxAge } } diff --git a/pkg/client/rp/verifier_test.go b/pkg/client/rp/verifier_test.go new file mode 100644 index 0000000..7588c1f --- /dev/null +++ b/pkg/client/rp/verifier_test.go @@ -0,0 +1,339 @@ +package rp + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + tu "github.com/zitadel/oidc/v2/internal/testutil" + "github.com/zitadel/oidc/v2/pkg/oidc" + "gopkg.in/square/go-jose.v2" +) + +func TestVerifyTokens(t *testing.T) { + verifier := &idTokenVerifier{ + issuer: tu.ValidIssuer, + maxAgeIAT: 2 * time.Minute, + offset: time.Second, + supportedSignAlgs: []string{string(tu.SignatureAlgorithm)}, + keySet: tu.KeySet{}, + maxAge: 2 * time.Minute, + acr: tu.ACRVerify, + nonce: func(context.Context) string { return tu.ValidNonce }, + clientID: tu.ValidClientID, + } + accessToken, _ := tu.ValidAccessToken() + atHash, err := oidc.ClaimHash(accessToken, tu.SignatureAlgorithm) + require.NoError(t, err) + + tests := []struct { + name string + accessToken string + idTokenClaims func() (string, *oidc.IDTokenClaims) + wantErr bool + }{ + { + name: "without access token", + idTokenClaims: tu.ValidIDToken, + }, + { + name: "with access token", + accessToken: accessToken, + idTokenClaims: func() (string, *oidc.IDTokenClaims) { + return tu.NewIDToken( + tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce, + tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, atHash, + ) + }, + }, + { + name: "expired id token", + accessToken: accessToken, + idTokenClaims: func() (string, *oidc.IDTokenClaims) { + return tu.NewIDToken( + tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration.Add(-time.Hour), tu.ValidAuthTime, tu.ValidNonce, + tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, atHash, + ) + }, + wantErr: true, + }, + { + name: "wrong access token", + accessToken: accessToken, + idTokenClaims: func() (string, *oidc.IDTokenClaims) { + return tu.NewIDToken( + tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce, + tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "~~~", + ) + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + idToken, want := tt.idTokenClaims() + got, err := VerifyTokens[*oidc.IDTokenClaims](context.Background(), tt.accessToken, idToken, verifier) + if tt.wantErr { + assert.Error(t, err) + assert.Nil(t, got) + return + } + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, got, want) + }) + } +} + +func TestVerifyIDToken(t *testing.T) { + verifier := &idTokenVerifier{ + issuer: tu.ValidIssuer, + maxAgeIAT: 2 * time.Minute, + offset: time.Second, + supportedSignAlgs: []string{string(tu.SignatureAlgorithm)}, + keySet: tu.KeySet{}, + maxAge: 2 * time.Minute, + acr: tu.ACRVerify, + nonce: func(context.Context) string { return tu.ValidNonce }, + } + + tests := []struct { + name string + clientID string + tokenClaims func() (string, *oidc.IDTokenClaims) + wantErr bool + }{ + { + name: "success", + clientID: tu.ValidClientID, + tokenClaims: tu.ValidIDToken, + }, + { + name: "parse err", + clientID: tu.ValidClientID, + tokenClaims: func() (string, *oidc.IDTokenClaims) { return "~~~~", nil }, + wantErr: true, + }, + { + name: "invalid signature", + clientID: tu.ValidClientID, + tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.InvalidSignatureToken, nil }, + wantErr: true, + }, + { + name: "empty subject", + clientID: tu.ValidClientID, + tokenClaims: func() (string, *oidc.IDTokenClaims) { + return tu.NewIDToken( + tu.ValidIssuer, "", tu.ValidAudience, + tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce, + tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "", + ) + }, + wantErr: true, + }, + { + name: "wrong issuer", + clientID: tu.ValidClientID, + tokenClaims: func() (string, *oidc.IDTokenClaims) { + return tu.NewIDToken( + "foo", tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce, + tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "", + ) + }, + wantErr: true, + }, + { + name: "wrong clientID", + clientID: "foo", + tokenClaims: tu.ValidIDToken, + wantErr: true, + }, + { + name: "expired", + clientID: tu.ValidClientID, + tokenClaims: func() (string, *oidc.IDTokenClaims) { + return tu.NewIDToken( + tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration.Add(-time.Hour), tu.ValidAuthTime, tu.ValidNonce, + tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "", + ) + }, + wantErr: true, + }, + { + name: "wrong IAT", + clientID: tu.ValidClientID, + tokenClaims: func() (string, *oidc.IDTokenClaims) { + return tu.NewIDToken( + tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce, + tu.ValidACR, tu.ValidAMR, tu.ValidClientID, -time.Hour, "", + ) + }, + wantErr: true, + }, + { + name: "wrong acr", + clientID: tu.ValidClientID, + tokenClaims: func() (string, *oidc.IDTokenClaims) { + return tu.NewIDToken( + tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce, + "else", tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "", + ) + }, + wantErr: true, + }, + { + name: "expired auth", + clientID: tu.ValidClientID, + tokenClaims: func() (string, *oidc.IDTokenClaims) { + return tu.NewIDToken( + tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration, tu.ValidAuthTime.Add(-time.Hour), tu.ValidNonce, + tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "", + ) + }, + wantErr: true, + }, + { + name: "wrong nonce", + clientID: tu.ValidClientID, + tokenClaims: func() (string, *oidc.IDTokenClaims) { + return tu.NewIDToken( + tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration, tu.ValidAuthTime, "foo", + tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "", + ) + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + token, want := tt.tokenClaims() + verifier.clientID = tt.clientID + got, err := VerifyIDToken[*oidc.IDTokenClaims](context.Background(), token, verifier) + if tt.wantErr { + assert.Error(t, err) + assert.Nil(t, got) + return + } + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, got, want) + }) + } +} + +func TestVerifyAccessToken(t *testing.T) { + token, _ := tu.ValidAccessToken() + hash, err := oidc.ClaimHash(token, tu.SignatureAlgorithm) + require.NoError(t, err) + + type args struct { + accessToken string + atHash string + sigAlgorithm jose.SignatureAlgorithm + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty hash", + }, + { + name: "success", + args: args{ + accessToken: token, + atHash: hash, + sigAlgorithm: tu.SignatureAlgorithm, + }, + }, + { + name: "invalid algorithm", + args: args{ + accessToken: token, + atHash: hash, + sigAlgorithm: "foo", + }, + wantErr: true, + }, + { + name: "mismatch", + args: args{ + accessToken: token, + atHash: "~~", + sigAlgorithm: tu.SignatureAlgorithm, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := VerifyAccessToken(tt.args.accessToken, tt.args.atHash, tt.args.sigAlgorithm) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestNewIDTokenVerifier(t *testing.T) { + type args struct { + issuer string + clientID string + keySet oidc.KeySet + options []VerifierOption + } + tests := []struct { + name string + args args + want IDTokenVerifier + }{ + { + name: "nil nonce", // otherwise assert.Equal will fail on the function + args: args{ + issuer: tu.ValidIssuer, + clientID: tu.ValidClientID, + keySet: tu.KeySet{}, + options: []VerifierOption{ + WithIssuedAtOffset(time.Minute), + WithIssuedAtMaxAge(time.Hour), + WithNonce(nil), // otherwise assert.Equal will fail on the function + WithACRVerifier(nil), + WithAuthTimeMaxAge(2 * time.Hour), + WithSupportedSigningAlgorithms("ABC", "DEF"), + }, + }, + want: &idTokenVerifier{ + issuer: tu.ValidIssuer, + offset: time.Minute, + maxAgeIAT: time.Hour, + clientID: tu.ValidClientID, + keySet: tu.KeySet{}, + nonce: nil, + acr: nil, + maxAge: 2 * time.Hour, + supportedSignAlgs: []string{"ABC", "DEF"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewIDTokenVerifier(tt.args.issuer, tt.args.clientID, tt.args.keySet, tt.args.options...) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/client/rp/verifier_tokens_example_test.go b/pkg/client/rp/verifier_tokens_example_test.go new file mode 100644 index 0000000..c297efe --- /dev/null +++ b/pkg/client/rp/verifier_tokens_example_test.go @@ -0,0 +1,86 @@ +package rp_test + +import ( + "context" + "fmt" + + tu "github.com/zitadel/oidc/v2/internal/testutil" + "github.com/zitadel/oidc/v2/pkg/client/rp" + "github.com/zitadel/oidc/v2/pkg/oidc" +) + +// MyCustomClaims extends the TokenClaims base, +// so it implmeents the oidc.Claims interface. +// Instead of carrying a map, we add needed fields// to the struct for type safe access. +type MyCustomClaims struct { + oidc.TokenClaims + NotBefore oidc.Time `json:"nbf,omitempty"` + AccessTokenHash string `json:"at_hash,omitempty"` + Foo string `json:"foo,omitempty"` + Bar *Nested `json:"bar,omitempty"` +} + +// GetAccessTokenHash is required to implement +// the oidc.IDClaims interface. +func (c *MyCustomClaims) GetAccessTokenHash() string { + return c.AccessTokenHash +} + +// Nested struct types are also possible. +type Nested struct { + Count int `json:"count,omitempty"` + Tags []string `json:"tags,omitempty"` +} + +/* +idToken carries the following claims. foo and bar are custom claims + + { + "acr": "something", + "amr": [ + "foo", + "bar" + ], + "at_hash": "2dzbm_vIxy-7eRtqUIGPPw", + "aud": [ + "unit", + "test", + "555666" + ], + "auth_time": 1678100961, + "azp": "555666", + "bar": { + "count": 22, + "tags": [ + "some", + "tags" + ] + }, + "client_id": "555666", + "exp": 4802238682, + "foo": "Hello, World!", + "iat": 1678101021, + "iss": "local.com", + "jti": "9876", + "nbf": 1678101021, + "nonce": "12345", + "sub": "tim@local.com" + } +*/ +const idToken = `eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJhY3IiOiJzb21ldGhpbmciLCJhbXIiOlsiZm9vIiwiYmFyIl0sImF0X2hhc2giOiIyZHpibV92SXh5LTdlUnRxVUlHUFB3IiwiYXVkIjpbInVuaXQiLCJ0ZXN0IiwiNTU1NjY2Il0sImF1dGhfdGltZSI6MTY3ODEwMDk2MSwiYXpwIjoiNTU1NjY2IiwiYmFyIjp7ImNvdW50IjoyMiwidGFncyI6WyJzb21lIiwidGFncyJdfSwiY2xpZW50X2lkIjoiNTU1NjY2IiwiZXhwIjo0ODAyMjM4NjgyLCJmb28iOiJIZWxsbywgV29ybGQhIiwiaWF0IjoxNjc4MTAxMDIxLCJpc3MiOiJsb2NhbC5jb20iLCJqdGkiOiI5ODc2IiwibmJmIjoxNjc4MTAxMDIxLCJub25jZSI6IjEyMzQ1Iiwic3ViIjoidGltQGxvY2FsLmNvbSJ9.t3GXSfVNNwiW1Suv9_84v0sdn2_-RWHVxhphhRozDXnsO7SDNOlGnEioemXABESxSzMclM7gB7mYy5Qah2ZUNx7eP5t2njoxEYfavgHwx7UJZ2NCg8NDPQyr-hlxelEcfdXK-I0oTd-FRDvF4rqPkD9Us52IpnplChCxnHFgh4wKwPqZZjv2IXVCtn0ilKW3hff1rMOYKEuLRcN2YP0gkyuqyHvcf2dMmjod0t4sLOTJ82rsCbMBC5CLpqv3nIC9HOGITkt1Kd-Am0n1LrdZvWwTo6RFe8AnzF0gpqjcB5Wg4Qeh58DIjZOz4f_8wnmJ_gCqyRh5vfSW4XHdbum0Tw` +const accessToken = `eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJhdWQiOlsidW5pdCIsInRlc3QiXSwiYmFyIjp7ImNvdW50IjoyMiwidGFncyI6WyJzb21lIiwidGFncyJdfSwiZXhwIjo0ODAyMjM4NjgyLCJmb28iOiJIZWxsbywgV29ybGQhIiwiaWF0IjoxNjc4MTAxMDIxLCJpc3MiOiJsb2NhbC5jb20iLCJqdGkiOiI5ODc2IiwibmJmIjoxNjc4MTAxMDIxLCJzdWIiOiJ0aW1AbG9jYWwuY29tIn0.Zrz3LWSRjCMJZUMaI5dUbW4vGdSmEeJQ3ouhaX0bcW9rdFFLgBI4K2FWJhNivq8JDmCGSxwLu3mI680GWmDaEoAx1M5sCO9lqfIZHGZh-lfAXk27e6FPLlkTDBq8Bx4o4DJ9Fw0hRJGjUTjnYv5cq1vo2-UqldasL6CwTbkzNC_4oQFfRtuodC4Ql7dZ1HRv5LXuYx7KPkOssLZtV9cwtJp5nFzKjcf2zEE_tlbjcpynMwypornRUp1EhCWKRUGkJhJeiP71ECY5pQhShfjBu9Nc5wDpSnZmnk2S4YsPrRK3QkE-iEkas8BfsOCrGoErHjEJexAIDjasGO5PFLWfCA` + +func ExampleVerifyTokens_customClaims() { + v := rp.NewIDTokenVerifier("local.com", "555666", tu.KeySet{}, + rp.WithNonce(func(ctx context.Context) string { return "12345" }), + ) + + // VerifyAccessToken can be called with the *MyCustomClaims. + claims, err := rp.VerifyTokens[*MyCustomClaims](context.TODO(), accessToken, idToken, v) + if err != nil { + panic(err) + } + // Here we have typesafe access to the custom claims + fmt.Println(claims.Foo, claims.Bar.Count, claims.Bar.Tags) + // Output: Hello, World! 22 [some tags] +} diff --git a/pkg/client/rs/resource_server.go b/pkg/client/rs/resource_server.go index 95a0121..4e0353c 100644 --- a/pkg/client/rs/resource_server.go +++ b/pkg/client/rs/resource_server.go @@ -112,7 +112,7 @@ func WithStaticEndpoints(tokenURL, introspectURL string) Option { } } -func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.IntrospectionResponse, error) { +func Introspect(ctx context.Context, rp ResourceServer, token string) (*oidc.IntrospectionResponse, error) { authFn, err := rp.AuthFn() if err != nil { return nil, err @@ -121,7 +121,7 @@ func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.Intr if err != nil { return nil, err } - resp := oidc.NewIntrospectionResponse() + resp := new(oidc.IntrospectionResponse) if err := httphelper.HttpRequest(rp.HttpClient(), req, resp); err != nil { return nil, err } diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index b7c220c..8313dc4 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -1,12 +1,6 @@ package oidc -import ( - "encoding/json" - "fmt" - "time" - - "golang.org/x/text/language" -) +import "github.com/muhlemmer/gu" type IntrospectionRequest struct { Token string `schema:"token"` @@ -17,36 +11,11 @@ type ClientAssertionParams struct { ClientAssertionType string `schema:"client_assertion_type"` } -type IntrospectionResponse interface { - UserInfoSetter - IsActive() bool - SetActive(bool) - SetScopes(scopes []string) - SetClientID(id string) - SetTokenType(tokenType string) - SetExpiration(exp time.Time) - SetIssuedAt(iat time.Time) - SetNotBefore(nbf time.Time) - SetAudience(audience []string) - SetIssuer(issuer string) - SetJWTID(id string) - GetScope() []string - GetClientID() string - GetTokenType() string - GetExpiration() time.Time - GetIssuedAt() time.Time - GetNotBefore() time.Time - GetSubject() string - GetAudience() []string - GetIssuer() string - GetJWTID() string -} - -func NewIntrospectionResponse() IntrospectionResponse { - return &introspectionResponse{} -} - -type introspectionResponse struct { +// IntrospectionResponse implements RFC 7662, section 2.2 and +// OpenID Connect Core 1.0, section 5.1 (UserInfo). +// https://www.rfc-editor.org/rfc/rfc7662.html#section-2.2. +// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims. +type IntrospectionResponse struct { Active bool `json:"active"` Scope SpaceDelimitedArray `json:"scope,omitempty"` ClientID string `json:"client_id,omitempty"` @@ -58,323 +27,50 @@ type introspectionResponse struct { Audience Audience `json:"aud,omitempty"` Issuer string `json:"iss,omitempty"` JWTID string `json:"jti,omitempty"` - userInfoProfile - userInfoEmail - userInfoPhone + Username string `json:"username,omitempty"` + UserInfoProfile + UserInfoEmail + UserInfoPhone - Address UserInfoAddress `json:"address,omitempty"` - claims map[string]interface{} + Address *UserInfoAddress `json:"address,omitempty"` + Claims map[string]any `json:"-"` } -func (i *introspectionResponse) IsActive() bool { - return i.Active +// SetUserInfo copies all relevant fields from UserInfo +// into the IntroSpectionResponse. +func (i *IntrospectionResponse) SetUserInfo(u *UserInfo) { + i.Subject = u.Subject + i.Username = u.PreferredUsername + i.Address = gu.PtrCopy(u.Address) + i.UserInfoProfile = u.UserInfoProfile + i.UserInfoEmail = u.UserInfoEmail + i.UserInfoPhone = u.UserInfoPhone + if i.Claims == nil { + i.Claims = gu.MapCopy(u.Claims) + } else { + gu.MapMerge(u.Claims, i.Claims) + } } -func (i *introspectionResponse) GetSubject() string { - return i.Subject -} - -func (i *introspectionResponse) GetName() string { - return i.Name -} - -func (i *introspectionResponse) GetGivenName() string { - return i.GivenName -} - -func (i *introspectionResponse) GetFamilyName() string { - return i.FamilyName -} - -func (i *introspectionResponse) GetMiddleName() string { - return i.MiddleName -} - -func (i *introspectionResponse) GetNickname() string { - return i.Nickname -} - -func (i *introspectionResponse) GetProfile() string { - return i.Profile -} - -func (i *introspectionResponse) GetPicture() string { - return i.Picture -} - -func (i *introspectionResponse) GetWebsite() string { - return i.Website -} - -func (i *introspectionResponse) GetGender() Gender { - return i.Gender -} - -func (i *introspectionResponse) GetBirthdate() string { - return i.Birthdate -} - -func (i *introspectionResponse) GetZoneinfo() string { - return i.Zoneinfo -} - -func (i *introspectionResponse) GetLocale() language.Tag { - return i.Locale -} - -func (i *introspectionResponse) GetPreferredUsername() string { - return i.PreferredUsername -} - -func (i *introspectionResponse) GetEmail() string { - return i.Email -} - -func (i *introspectionResponse) IsEmailVerified() bool { - return bool(i.EmailVerified) -} - -func (i *introspectionResponse) GetPhoneNumber() string { - return i.PhoneNumber -} - -func (i *introspectionResponse) IsPhoneNumberVerified() bool { - return i.PhoneNumberVerified -} - -func (i *introspectionResponse) GetAddress() UserInfoAddress { +// GetAddress is a safe getter that takes +// care of a possible nil value. +func (i *IntrospectionResponse) GetAddress() *UserInfoAddress { + if i.Address == nil { + return new(UserInfoAddress) + } return i.Address } -func (i *introspectionResponse) GetClaim(key string) interface{} { - return i.claims[key] -} +// introspectionResponseAlias prevents loops on the JSON methods +type introspectionResponseAlias IntrospectionResponse -func (i *introspectionResponse) GetClaims() map[string]interface{} { - return i.claims -} - -func (i *introspectionResponse) GetScope() []string { - return []string(i.Scope) -} - -func (i *introspectionResponse) GetClientID() string { - return i.ClientID -} - -func (i *introspectionResponse) GetTokenType() string { - return i.TokenType -} - -func (i *introspectionResponse) GetExpiration() time.Time { - return time.Time(i.Expiration) -} - -func (i *introspectionResponse) GetIssuedAt() time.Time { - return time.Time(i.IssuedAt) -} - -func (i *introspectionResponse) GetNotBefore() time.Time { - return time.Time(i.NotBefore) -} - -func (i *introspectionResponse) GetAudience() []string { - return []string(i.Audience) -} - -func (i *introspectionResponse) GetIssuer() string { - return i.Issuer -} - -func (i *introspectionResponse) GetJWTID() string { - return i.JWTID -} - -func (i *introspectionResponse) SetActive(active bool) { - i.Active = active -} - -func (i *introspectionResponse) SetScopes(scope []string) { - i.Scope = scope -} - -func (i *introspectionResponse) SetClientID(id string) { - i.ClientID = id -} - -func (i *introspectionResponse) SetTokenType(tokenType string) { - i.TokenType = tokenType -} - -func (i *introspectionResponse) SetExpiration(exp time.Time) { - i.Expiration = Time(exp) -} - -func (i *introspectionResponse) SetIssuedAt(iat time.Time) { - i.IssuedAt = Time(iat) -} - -func (i *introspectionResponse) SetNotBefore(nbf time.Time) { - i.NotBefore = Time(nbf) -} - -func (i *introspectionResponse) SetAudience(audience []string) { - i.Audience = audience -} - -func (i *introspectionResponse) SetIssuer(issuer string) { - i.Issuer = issuer -} - -func (i *introspectionResponse) SetJWTID(id string) { - i.JWTID = id -} - -func (i *introspectionResponse) SetSubject(sub string) { - i.Subject = sub -} - -func (i *introspectionResponse) SetName(name string) { - i.Name = name -} - -func (i *introspectionResponse) SetGivenName(name string) { - i.GivenName = name -} - -func (i *introspectionResponse) SetFamilyName(name string) { - i.FamilyName = name -} - -func (i *introspectionResponse) SetMiddleName(name string) { - i.MiddleName = name -} - -func (i *introspectionResponse) SetNickname(name string) { - i.Nickname = name -} - -func (i *introspectionResponse) SetUpdatedAt(date time.Time) { - i.UpdatedAt = Time(date) -} - -func (i *introspectionResponse) SetProfile(profile string) { - i.Profile = profile -} - -func (i *introspectionResponse) SetPicture(picture string) { - i.Picture = picture -} - -func (i *introspectionResponse) SetWebsite(website string) { - i.Website = website -} - -func (i *introspectionResponse) SetGender(gender Gender) { - i.Gender = gender -} - -func (i *introspectionResponse) SetBirthdate(birthdate string) { - i.Birthdate = birthdate -} - -func (i *introspectionResponse) SetZoneinfo(zoneInfo string) { - i.Zoneinfo = zoneInfo -} - -func (i *introspectionResponse) SetLocale(locale language.Tag) { - i.Locale = locale -} - -func (i *introspectionResponse) SetPreferredUsername(name string) { - i.PreferredUsername = name -} - -func (i *introspectionResponse) SetEmail(email string, verified bool) { - i.Email = email - i.EmailVerified = boolString(verified) -} - -func (i *introspectionResponse) SetPhone(phone string, verified bool) { - i.PhoneNumber = phone - i.PhoneNumberVerified = verified -} - -func (i *introspectionResponse) SetAddress(address UserInfoAddress) { - i.Address = address -} - -func (i *introspectionResponse) AppendClaims(key string, value interface{}) { - if i.claims == nil { - i.claims = make(map[string]interface{}) +func (i *IntrospectionResponse) MarshalJSON() ([]byte, error) { + if i.Username == "" { + i.Username = i.PreferredUsername } - i.claims[key] = value + return mergeAndMarshalClaims((*introspectionResponseAlias)(i), i.Claims) } -func (i *introspectionResponse) MarshalJSON() ([]byte, error) { - type Alias introspectionResponse - a := &struct { - *Alias - Expiration int64 `json:"exp,omitempty"` - IssuedAt int64 `json:"iat,omitempty"` - NotBefore int64 `json:"nbf,omitempty"` - Locale interface{} `json:"locale,omitempty"` - UpdatedAt int64 `json:"updated_at,omitempty"` - Username string `json:"username,omitempty"` - }{ - Alias: (*Alias)(i), - } - if !i.Locale.IsRoot() { - a.Locale = i.Locale - } - if !time.Time(i.UpdatedAt).IsZero() { - a.UpdatedAt = time.Time(i.UpdatedAt).Unix() - } - if !time.Time(i.Expiration).IsZero() { - a.Expiration = time.Time(i.Expiration).Unix() - } - if !time.Time(i.IssuedAt).IsZero() { - a.IssuedAt = time.Time(i.IssuedAt).Unix() - } - if !time.Time(i.NotBefore).IsZero() { - a.NotBefore = time.Time(i.NotBefore).Unix() - } - a.Username = i.PreferredUsername - - b, err := json.Marshal(a) - if err != nil { - return nil, err - } - - if len(i.claims) == 0 { - return b, nil - } - - err = json.Unmarshal(b, &i.claims) - if err != nil { - return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims) - } - - return json.Marshal(i.claims) -} - -func (i *introspectionResponse) UnmarshalJSON(data []byte) error { - type Alias introspectionResponse - a := &struct { - *Alias - UpdatedAt int64 `json:"update_at,omitempty"` - }{ - Alias: (*Alias)(i), - } - if err := json.Unmarshal(data, &a); err != nil { - return err - } - - i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC()) - - if err := json.Unmarshal(data, &i.claims); err != nil { - return err - } - - return nil +func (i *IntrospectionResponse) UnmarshalJSON(data []byte) error { + return unmarshalJSONMulti(data, (*introspectionResponseAlias)(i), &i.Claims) } diff --git a/pkg/oidc/introspection_test.go b/pkg/oidc/introspection_test.go new file mode 100644 index 0000000..bd49894 --- /dev/null +++ b/pkg/oidc/introspection_test.go @@ -0,0 +1,78 @@ +package oidc + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIntrospectionResponse_SetUserInfo(t *testing.T) { + tests := []struct { + name string + start *IntrospectionResponse + want *IntrospectionResponse + }{ + { + + name: "nil claims", + start: &IntrospectionResponse{}, + want: &IntrospectionResponse{ + Subject: userInfoData.Subject, + Username: userInfoData.PreferredUsername, + Address: userInfoData.Address, + UserInfoProfile: userInfoData.UserInfoProfile, + UserInfoEmail: userInfoData.UserInfoEmail, + UserInfoPhone: userInfoData.UserInfoPhone, + Claims: userInfoData.Claims, + }, + }, + { + + name: "merge claims", + start: &IntrospectionResponse{ + Claims: map[string]any{ + "hello": "world", + }, + }, + want: &IntrospectionResponse{ + Subject: userInfoData.Subject, + Username: userInfoData.PreferredUsername, + Address: userInfoData.Address, + UserInfoProfile: userInfoData.UserInfoProfile, + UserInfoEmail: userInfoData.UserInfoEmail, + UserInfoPhone: userInfoData.UserInfoPhone, + Claims: map[string]any{ + "foo": "bar", + "hello": "world", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.start.SetUserInfo(userInfoData) + assert.Equal(t, tt.want, tt.start) + }) + } +} + +func TestIntrospectionResponse_GetAddress(t *testing.T) { + // nil address + i := new(IntrospectionResponse) + assert.Equal(t, &UserInfoAddress{}, i.GetAddress()) + + i.Address = &UserInfoAddress{PostalCode: "1234"} + assert.Equal(t, i.Address, i.GetAddress()) +} + +func TestIntrospectionResponse_MarshalJSON(t *testing.T) { + got, err := json.Marshal(&IntrospectionResponse{ + UserInfoProfile: UserInfoProfile{ + PreferredUsername: "muhlemmer", + }, + }) + require.NoError(t, err) + assert.Equal(t, string(got), `{"active":false,"username":"muhlemmer","preferred_username":"muhlemmer"}`) +} diff --git a/pkg/oidc/regression_assert_test.go b/pkg/oidc/regression_assert_test.go new file mode 100644 index 0000000..5e9fb3d --- /dev/null +++ b/pkg/oidc/regression_assert_test.go @@ -0,0 +1,50 @@ +//go:build !create_regression_data + +package oidc + +import ( + "encoding/json" + "io" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Test_assert_regression verifies current output from +// json.Marshal to stored regression data. +// These tests are only ran when the create_regression_data +// tag is NOT set. +func Test_assert_regression(t *testing.T) { + buf := new(strings.Builder) + + for _, obj := range regressionData { + name := jsonFilename(obj) + t.Run(name, func(t *testing.T) { + file, err := os.Open(name) + require.NoError(t, err) + defer file.Close() + + _, err = io.Copy(buf, file) + require.NoError(t, err) + want := buf.String() + buf.Reset() + + encodeJSON(t, buf, obj) + first := buf.String() + buf.Reset() + + assert.JSONEq(t, want, first) + + require.NoError(t, + json.Unmarshal([]byte(first), obj), + ) + second, err := json.Marshal(obj) + require.NoError(t, err) + + assert.JSONEq(t, want, string(second)) + }) + } +} diff --git a/pkg/oidc/regression_create_test.go b/pkg/oidc/regression_create_test.go new file mode 100644 index 0000000..809fe60 --- /dev/null +++ b/pkg/oidc/regression_create_test.go @@ -0,0 +1,24 @@ +//go:build create_regression_data + +package oidc + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +// Test_create_regression generates the regression data. +// It is excluded from regular testing, unless +// called with the create_regression_data tag: +// go test -tags="create_regression_data" ./pkg/oidc +func Test_create_regression(t *testing.T) { + for _, obj := range regressionData { + file, err := os.Create(jsonFilename(obj)) + require.NoError(t, err) + defer file.Close() + + encodeJSON(t, file, obj) + } +} diff --git a/pkg/oidc/regression_data/oidc.AccessTokenClaims.json b/pkg/oidc/regression_data/oidc.AccessTokenClaims.json new file mode 100644 index 0000000..e4f7808 --- /dev/null +++ b/pkg/oidc/regression_data/oidc.AccessTokenClaims.json @@ -0,0 +1,26 @@ +{ + "iss": "zitadel", + "sub": "hello@me.com", + "aud": [ + "foo", + "bar" + ], + "jti": "900", + "azp": "just@me.com", + "nonce": "6969", + "acr": "something", + "amr": [ + "some", + "methods" + ], + "scope": [ + "email", + "phone" + ], + "client_id": "777", + "exp": 12345, + "iat": 12000, + "nbf": 12000, + "auth_time": 12000, + "foo": "bar" +} diff --git a/pkg/oidc/regression_data/oidc.IDTokenClaims.json b/pkg/oidc/regression_data/oidc.IDTokenClaims.json new file mode 100644 index 0000000..af503fb --- /dev/null +++ b/pkg/oidc/regression_data/oidc.IDTokenClaims.json @@ -0,0 +1,51 @@ +{ + "iss": "zitadel", + "aud": [ + "foo", + "bar" + ], + "jti": "900", + "azp": "just@me.com", + "nonce": "6969", + "at_hash": "acthashhash", + "c_hash": "hashhash", + "acr": "something", + "amr": [ + "some", + "methods" + ], + "sid": "666", + "client_id": "777", + "exp": 12345, + "iat": 12000, + "nbf": 12000, + "auth_time": 12000, + "address": { + "country": "Moon", + "formatted": "Sesame street 666\n666-666, Smallvile\nMoon", + "locality": "Smallvile", + "postal_code": "666-666", + "region": "Outer space", + "street_address": "Sesame street 666" + }, + "birthdate": "1st of April", + "email": "tim@zitadel.com", + "email_verified": true, + "family_name": "Möhlmann", + "foo": "bar", + "gender": "male", + "given_name": "Tim", + "locale": "nl", + "middle_name": "Danger", + "name": "Tim Möhlmann", + "nickname": "muhlemmer", + "phone_number": "+1234567890", + "phone_number_verified": true, + "picture": "https://avatars.githubusercontent.com/u/5411563?v=4", + "preferred_username": "muhlemmer", + "profile": "https://github.com/muhlemmer", + "sub": "hello@me.com", + "updated_at": 1, + "website": "https://zitadel.com", + "zoneinfo": "Europe/Amsterdam" +} diff --git a/pkg/oidc/regression_data/oidc.IntrospectionResponse.json b/pkg/oidc/regression_data/oidc.IntrospectionResponse.json new file mode 100644 index 0000000..e0c21a2 --- /dev/null +++ b/pkg/oidc/regression_data/oidc.IntrospectionResponse.json @@ -0,0 +1,44 @@ +{ + "active": true, + "address": { + "country": "Moon", + "formatted": "Sesame street 666\n666-666, Smallvile\nMoon", + "locality": "Smallvile", + "postal_code": "666-666", + "region": "Outer space", + "street_address": "Sesame street 666" + }, + "aud": [ + "foo", + "bar" + ], + "birthdate": "1st of April", + "client_id": "777", + "email": "tim@zitadel.com", + "email_verified": true, + "exp": 12345, + "family_name": "Möhlmann", + "foo": "bar", + "gender": "male", + "given_name": "Tim", + "iat": 12000, + "iss": "zitadel", + "jti": "900", + "locale": "nl", + "middle_name": "Danger", + "name": "Tim Möhlmann", + "nbf": 12000, + "nickname": "muhlemmer", + "phone_number": "+1234567890", + "phone_number_verified": true, + "picture": "https://avatars.githubusercontent.com/u/5411563?v=4", + "preferred_username": "muhlemmer", + "profile": "https://github.com/muhlemmer", + "scope": "email phone", + "sub": "hello@me.com", + "token_type": "idtoken", + "updated_at": 1, + "username": "muhlemmer", + "website": "https://zitadel.com", + "zoneinfo": "Europe/Amsterdam" +} diff --git a/pkg/oidc/regression_data/oidc.JWTProfileAssertionClaims.json b/pkg/oidc/regression_data/oidc.JWTProfileAssertionClaims.json new file mode 100644 index 0000000..4ece780 --- /dev/null +++ b/pkg/oidc/regression_data/oidc.JWTProfileAssertionClaims.json @@ -0,0 +1,11 @@ +{ + "aud": [ + "foo", + "bar" + ], + "exp": 12345, + "foo": "bar", + "iat": 12000, + "iss": "zitadel", + "sub": "hello@me.com" +} diff --git a/pkg/oidc/regression_data/oidc.UserInfo.json b/pkg/oidc/regression_data/oidc.UserInfo.json new file mode 100644 index 0000000..d7795e7 --- /dev/null +++ b/pkg/oidc/regression_data/oidc.UserInfo.json @@ -0,0 +1,30 @@ +{ + "address": { + "country": "Moon", + "formatted": "Sesame street 666\n666-666, Smallvile\nMoon", + "locality": "Smallvile", + "postal_code": "666-666", + "region": "Outer space", + "street_address": "Sesame street 666" + }, + "birthdate": "1st of April", + "email": "tim@zitadel.com", + "email_verified": true, + "family_name": "Möhlmann", + "foo": "bar", + "gender": "male", + "given_name": "Tim", + "locale": "nl", + "middle_name": "Danger", + "name": "Tim Möhlmann", + "nickname": "muhlemmer", + "phone_number": "+1234567890", + "phone_number_verified": true, + "picture": "https://avatars.githubusercontent.com/u/5411563?v=4", + "preferred_username": "muhlemmer", + "profile": "https://github.com/muhlemmer", + "sub": "hello@me.com", + "updated_at": 1, + "website": "https://zitadel.com", + "zoneinfo": "Europe/Amsterdam" +} diff --git a/pkg/oidc/regression_test.go b/pkg/oidc/regression_test.go new file mode 100644 index 0000000..5d33bb6 --- /dev/null +++ b/pkg/oidc/regression_test.go @@ -0,0 +1,40 @@ +package oidc + +// This file contains common functions and data for regression testing + +import ( + "encoding/json" + "fmt" + "io" + "path" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +const dataDir = "regression_data" + +// jsonFilename builds a filename for the regression testdata. +// dataDir/.json +func jsonFilename(obj interface{}) string { + name := fmt.Sprintf("%T.json", obj) + return path.Join( + dataDir, + strings.TrimPrefix(name, "*"), + ) +} + +func encodeJSON(t *testing.T, w io.Writer, obj interface{}) { + enc := json.NewEncoder(w) + enc.SetIndent("", "\t") + require.NoError(t, enc.Encode(obj)) +} + +var regressionData = []interface{}{ + accessTokenData, + idTokenData, + introspectionResponseData, + userInfoData, + jwtProfileAssertionData, +} diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index b538465..1ade913 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -2,15 +2,13 @@ package oidc import ( "encoding/json" - "fmt" - "io/ioutil" + "os" "time" "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" "github.com/zitadel/oidc/v2/pkg/crypto" - "github.com/zitadel/oidc/v2/pkg/http" ) const ( @@ -20,380 +18,174 @@ const ( PrefixBearer = BearerToken + " " ) -type Tokens struct { +type Tokens[C IDClaims] struct { *oauth2.Token - IDTokenClaims IDTokenClaims + IDTokenClaims C IDToken string } -type AccessTokenClaims interface { - Claims - GetSubject() string - GetTokenID() string - SetPrivateClaims(map[string]interface{}) - GetClaims() map[string]interface{} -} - -type IDTokenClaims interface { - Claims - GetNotBefore() time.Time - GetJWTID() string - GetAccessTokenHash() string - GetCodeHash() string - GetAuthenticationMethodsReferences() []string - GetClientID() string - GetSignatureAlgorithm() jose.SignatureAlgorithm - SetAccessTokenHash(hash string) - SetUserinfo(userinfo UserInfo) - SetCodeHash(hash string) - UserInfo -} - -func EmptyAccessTokenClaims() AccessTokenClaims { - return new(accessTokenClaims) -} - -func NewAccessTokenClaims(issuer, subject string, audience []string, expiration time.Time, id, clientID string, skew time.Duration) AccessTokenClaims { - now := time.Now().UTC().Add(-skew) - if len(audience) == 0 { - audience = append(audience, clientID) - } - return &accessTokenClaims{ - Issuer: issuer, - Subject: subject, - Audience: audience, - Expiration: Time(expiration), - IssuedAt: Time(now), - NotBefore: Time(now), - JWTID: id, - } -} - -type accessTokenClaims struct { +// TokenClaims contains the base Claims used all tokens. +// It implements OpenID Connect Core 1.0, section 2. +// https://openid.net/specs/openid-connect-core-1_0.html#IDToken +// And RFC 9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens, +// section 2.2. https://datatracker.ietf.org/doc/html/rfc9068#name-data-structure +// +// TokenClaims implements the Claims interface, +// and can be used to extend larger claim types by embedding. +type TokenClaims struct { Issuer string `json:"iss,omitempty"` Subject string `json:"sub,omitempty"` Audience Audience `json:"aud,omitempty"` Expiration Time `json:"exp,omitempty"` IssuedAt Time `json:"iat,omitempty"` - NotBefore Time `json:"nbf,omitempty"` - JWTID string `json:"jti,omitempty"` - AuthorizedParty string `json:"azp,omitempty"` - Nonce string `json:"nonce,omitempty"` AuthTime Time `json:"auth_time,omitempty"` - CodeHash string `json:"c_hash,omitempty"` + NotBefore Time `json:"nbf,omitempty"` + Nonce string `json:"nonce,omitempty"` AuthenticationContextClassReference string `json:"acr,omitempty"` AuthenticationMethodsReferences []string `json:"amr,omitempty"` - SessionID string `json:"sid,omitempty"` - Scopes []string `json:"scope,omitempty"` - ClientID string `json:"client_id,omitempty"` - AccessTokenUseNumber int `json:"at_use_nbr,omitempty"` - - claims map[string]interface{} `json:"-"` - signatureAlg jose.SignatureAlgorithm `json:"-"` -} - -// GetIssuer implements the Claims interface -func (a *accessTokenClaims) GetIssuer() string { - return a.Issuer -} - -// GetAudience implements the Claims interface -func (a *accessTokenClaims) GetAudience() []string { - return a.Audience -} - -// GetExpiration implements the Claims interface -func (a *accessTokenClaims) GetExpiration() time.Time { - return time.Time(a.Expiration) -} - -// GetIssuedAt implements the Claims interface -func (a *accessTokenClaims) GetIssuedAt() time.Time { - return time.Time(a.IssuedAt) -} - -// GetNonce implements the Claims interface -func (a *accessTokenClaims) GetNonce() string { - return a.Nonce -} - -// GetAuthenticationContextClassReference implements the Claims interface -func (a *accessTokenClaims) GetAuthenticationContextClassReference() string { - return a.AuthenticationContextClassReference -} - -// GetAuthTime implements the Claims interface -func (a *accessTokenClaims) GetAuthTime() time.Time { - return time.Time(a.AuthTime) -} - -// GetAuthorizedParty implements the Claims interface -func (a *accessTokenClaims) GetAuthorizedParty() string { - return a.AuthorizedParty -} - -// SetSignatureAlgorithm implements the Claims interface -func (a *accessTokenClaims) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) { - a.signatureAlg = algorithm -} - -// GetSubject implements the AccessTokenClaims interface -func (a *accessTokenClaims) GetSubject() string { - return a.Subject -} - -// GetTokenID implements the AccessTokenClaims interface -func (a *accessTokenClaims) GetTokenID() string { - return a.JWTID -} - -// SetPrivateClaims implements the AccessTokenClaims interface -func (a *accessTokenClaims) SetPrivateClaims(claims map[string]interface{}) { - a.claims = claims -} - -// GetClaims implements the AccessTokenClaims interface -func (a *accessTokenClaims) GetClaims() map[string]interface{} { - return a.claims -} - -func (a *accessTokenClaims) MarshalJSON() ([]byte, error) { - type Alias accessTokenClaims - s := &struct { - *Alias - Expiration int64 `json:"exp,omitempty"` - IssuedAt int64 `json:"iat,omitempty"` - NotBefore int64 `json:"nbf,omitempty"` - AuthTime int64 `json:"auth_time,omitempty"` - }{ - Alias: (*Alias)(a), - } - if !time.Time(a.Expiration).IsZero() { - s.Expiration = time.Time(a.Expiration).Unix() - } - if !time.Time(a.IssuedAt).IsZero() { - s.IssuedAt = time.Time(a.IssuedAt).Unix() - } - if !time.Time(a.NotBefore).IsZero() { - s.NotBefore = time.Time(a.NotBefore).Unix() - } - if !time.Time(a.AuthTime).IsZero() { - s.AuthTime = time.Time(a.AuthTime).Unix() - } - b, err := json.Marshal(s) - if err != nil { - return nil, err - } - - if a.claims == nil { - return b, nil - } - info, err := json.Marshal(a.claims) - if err != nil { - return nil, err - } - return http.ConcatenateJSON(b, info) -} - -func (a *accessTokenClaims) UnmarshalJSON(data []byte) error { - type Alias accessTokenClaims - if err := json.Unmarshal(data, (*Alias)(a)); err != nil { - return err - } - claims := make(map[string]interface{}) - if err := json.Unmarshal(data, &claims); err != nil { - return err - } - a.claims = claims - - return nil -} - -func EmptyIDTokenClaims() IDTokenClaims { - return new(idTokenClaims) -} - -func NewIDTokenClaims(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string, skew time.Duration) IDTokenClaims { - audience = AppendClientIDToAudience(clientID, audience) - return &idTokenClaims{ - Issuer: issuer, - Audience: audience, - Expiration: Time(expiration), - IssuedAt: Time(time.Now().UTC().Add(-skew)), - AuthTime: Time(authTime.Add(-skew)), - Nonce: nonce, - AuthenticationContextClassReference: acr, - AuthenticationMethodsReferences: amr, - AuthorizedParty: clientID, - UserInfo: &userinfo{Subject: subject}, - } -} - -type idTokenClaims struct { - Issuer string `json:"iss,omitempty"` - Audience Audience `json:"aud,omitempty"` - Expiration Time `json:"exp,omitempty"` - NotBefore Time `json:"nbf,omitempty"` - IssuedAt Time `json:"iat,omitempty"` - JWTID string `json:"jti,omitempty"` AuthorizedParty string `json:"azp,omitempty"` - Nonce string `json:"nonce,omitempty"` - AuthTime Time `json:"auth_time,omitempty"` - AccessTokenHash string `json:"at_hash,omitempty"` - CodeHash string `json:"c_hash,omitempty"` - AuthenticationContextClassReference string `json:"acr,omitempty"` - AuthenticationMethodsReferences []string `json:"amr,omitempty"` ClientID string `json:"client_id,omitempty"` - UserInfo `json:"-"` + JWTID string `json:"jti,omitempty"` - signatureAlg jose.SignatureAlgorithm + // Additional information set by this framework + SignatureAlg jose.SignatureAlgorithm `json:"-"` } -// GetIssuer implements the Claims interface -func (t *idTokenClaims) GetIssuer() string { - return t.Issuer +func (c *TokenClaims) GetIssuer() string { + return c.Issuer } -// GetAudience implements the Claims interface -func (t *idTokenClaims) GetAudience() []string { - return t.Audience +func (c *TokenClaims) GetSubject() string { + return c.Subject } -// GetExpiration implements the Claims interface -func (t *idTokenClaims) GetExpiration() time.Time { - return time.Time(t.Expiration) +func (c *TokenClaims) GetAudience() []string { + return c.Audience } -// GetIssuedAt implements the Claims interface -func (t *idTokenClaims) GetIssuedAt() time.Time { - return time.Time(t.IssuedAt) +func (c *TokenClaims) GetExpiration() time.Time { + return c.Expiration.AsTime() } -// GetNonce implements the Claims interface -func (t *idTokenClaims) GetNonce() string { - return t.Nonce +func (c *TokenClaims) GetIssuedAt() time.Time { + return c.IssuedAt.AsTime() } -// GetAuthenticationContextClassReference implements the Claims interface -func (t *idTokenClaims) GetAuthenticationContextClassReference() string { - return t.AuthenticationContextClassReference +func (c *TokenClaims) GetNonce() string { + return c.Nonce } -// GetAuthTime implements the Claims interface -func (t *idTokenClaims) GetAuthTime() time.Time { - return time.Time(t.AuthTime) +func (c *TokenClaims) GetAuthTime() time.Time { + return c.AuthTime.AsTime() } -// GetAuthorizedParty implements the Claims interface -func (t *idTokenClaims) GetAuthorizedParty() string { - return t.AuthorizedParty +func (c *TokenClaims) GetAuthorizedParty() string { + return c.AuthorizedParty } -// SetSignatureAlgorithm implements the Claims interface -func (t *idTokenClaims) SetSignatureAlgorithm(alg jose.SignatureAlgorithm) { - t.signatureAlg = alg +func (c *TokenClaims) GetSignatureAlgorithm() jose.SignatureAlgorithm { + return c.SignatureAlg } -// GetNotBefore implements the IDTokenClaims interface -func (t *idTokenClaims) GetNotBefore() time.Time { - return time.Time(t.NotBefore) +func (c *TokenClaims) GetAuthenticationContextClassReference() string { + return c.AuthenticationContextClassReference } -// GetJWTID implements the IDTokenClaims interface -func (t *idTokenClaims) GetJWTID() string { - return t.JWTID +func (c *TokenClaims) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) { + c.SignatureAlg = algorithm +} + +type AccessTokenClaims struct { + TokenClaims + Scopes []string `json:"scope,omitempty"` + Claims map[string]any `json:"-"` +} + +func NewAccessTokenClaims(issuer, subject string, audience []string, expiration time.Time, jwtid, clientID string, skew time.Duration) *AccessTokenClaims { + now := time.Now().UTC().Add(-skew) + if len(audience) == 0 { + audience = append(audience, clientID) + } + return &AccessTokenClaims{ + TokenClaims: TokenClaims{ + Issuer: issuer, + Subject: subject, + Audience: audience, + Expiration: FromTime(expiration), + IssuedAt: FromTime(now), + NotBefore: FromTime(now), + JWTID: jwtid, + }, + } +} + +type atcAlias AccessTokenClaims + +func (a *AccessTokenClaims) MarshalJSON() ([]byte, error) { + return mergeAndMarshalClaims((*atcAlias)(a), a.Claims) +} + +func (a *AccessTokenClaims) UnmarshalJSON(data []byte) error { + return unmarshalJSONMulti(data, (*atcAlias)(a), &a.Claims) +} + +// IDTokenClaims extends TokenClaims by further implementing +// OpenID Connect Core 1.0, sections 3.1.3.6 (Code flow), +// 3.2.2.10 (implicit), 3.3.2.11 (Hybrid) and 5.1 (UserInfo). +// https://openid.net/specs/openid-connect-core-1_0.html#toc +type IDTokenClaims struct { + TokenClaims + NotBefore Time `json:"nbf,omitempty"` + AccessTokenHash string `json:"at_hash,omitempty"` + CodeHash string `json:"c_hash,omitempty"` + SessionID string `json:"sid,omitempty"` + UserInfoProfile + UserInfoEmail + UserInfoPhone + Address *UserInfoAddress `json:"address,omitempty"` + Claims map[string]any `json:"-"` } // GetAccessTokenHash implements the IDTokenClaims interface -func (t *idTokenClaims) GetAccessTokenHash() string { +func (t *IDTokenClaims) GetAccessTokenHash() string { return t.AccessTokenHash } -// GetCodeHash implements the IDTokenClaims interface -func (t *idTokenClaims) GetCodeHash() string { - return t.CodeHash +func (t *IDTokenClaims) SetUserInfo(i *UserInfo) { + t.Subject = i.Subject + t.UserInfoProfile = i.UserInfoProfile + t.UserInfoEmail = i.UserInfoEmail + t.UserInfoPhone = i.UserInfoPhone + t.Address = i.Address } -// GetAuthenticationMethodsReferences implements the IDTokenClaims interface -func (t *idTokenClaims) GetAuthenticationMethodsReferences() []string { - return t.AuthenticationMethodsReferences +func NewIDTokenClaims(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string, skew time.Duration) *IDTokenClaims { + audience = AppendClientIDToAudience(clientID, audience) + return &IDTokenClaims{ + TokenClaims: TokenClaims{ + Issuer: issuer, + Subject: subject, + Audience: audience, + Expiration: FromTime(expiration), + IssuedAt: FromTime(time.Now().Add(-skew)), + AuthTime: FromTime(authTime.Add(-skew)), + Nonce: nonce, + AuthenticationContextClassReference: acr, + AuthenticationMethodsReferences: amr, + AuthorizedParty: clientID, + ClientID: clientID, + }, + } } -// GetClientID implements the IDTokenClaims interface -func (t *idTokenClaims) GetClientID() string { - return t.ClientID +type itcAlias IDTokenClaims + +func (i *IDTokenClaims) MarshalJSON() ([]byte, error) { + return mergeAndMarshalClaims((*itcAlias)(i), i.Claims) } -// GetSignatureAlgorithm implements the IDTokenClaims interface -func (t *idTokenClaims) GetSignatureAlgorithm() jose.SignatureAlgorithm { - return t.signatureAlg -} - -// SetAccessTokenHash implements the IDTokenClaims interface -func (t *idTokenClaims) SetAccessTokenHash(hash string) { - t.AccessTokenHash = hash -} - -// SetUserinfo implements the IDTokenClaims interface -func (t *idTokenClaims) SetUserinfo(info UserInfo) { - t.UserInfo = info -} - -// SetCodeHash implements the IDTokenClaims interface -func (t *idTokenClaims) SetCodeHash(hash string) { - t.CodeHash = hash -} - -func (t *idTokenClaims) MarshalJSON() ([]byte, error) { - type Alias idTokenClaims - a := &struct { - *Alias - Expiration int64 `json:"exp,omitempty"` - IssuedAt int64 `json:"iat,omitempty"` - NotBefore int64 `json:"nbf,omitempty"` - AuthTime int64 `json:"auth_time,omitempty"` - }{ - Alias: (*Alias)(t), - } - if !time.Time(t.Expiration).IsZero() { - a.Expiration = time.Time(t.Expiration).Unix() - } - if !time.Time(t.IssuedAt).IsZero() { - a.IssuedAt = time.Time(t.IssuedAt).Unix() - } - if !time.Time(t.NotBefore).IsZero() { - a.NotBefore = time.Time(t.NotBefore).Unix() - } - if !time.Time(t.AuthTime).IsZero() { - a.AuthTime = time.Time(t.AuthTime).Unix() - } - b, err := json.Marshal(a) - if err != nil { - return nil, err - } - - if t.UserInfo == nil { - return b, nil - } - info, err := json.Marshal(t.UserInfo) - if err != nil { - return nil, err - } - return http.ConcatenateJSON(b, info) -} - -func (t *idTokenClaims) UnmarshalJSON(data []byte) error { - type Alias idTokenClaims - if err := json.Unmarshal(data, (*Alias)(t)); err != nil { - return err - } - userinfo := new(userinfo) - if err := json.Unmarshal(data, userinfo); err != nil { - return err - } - t.UserInfo = userinfo - - return nil +func (i *IDTokenClaims) UnmarshalJSON(data []byte) error { + return unmarshalJSONMulti(data, (*itcAlias)(i), &i.Claims) } type AccessTokenResponse struct { @@ -405,19 +197,7 @@ type AccessTokenResponse struct { State string `json:"state,omitempty" schema:"state,omitempty"` } -type JWTProfileAssertionClaims interface { - GetKeyID() string - GetPrivateKey() []byte - GetIssuer() string - GetSubject() string - GetAudience() []string - GetExpiration() time.Time - GetIssuedAt() time.Time - SetCustomClaim(key string, value interface{}) - GetCustomClaim(key string) interface{} -} - -type jwtProfileAssertion struct { +type JWTProfileAssertionClaims struct { PrivateKeyID string `json:"-"` PrivateKey []byte `json:"-"` Issuer string `json:"iss"` @@ -426,91 +206,21 @@ type jwtProfileAssertion struct { Expiration Time `json:"exp"` IssuedAt Time `json:"iat"` - customClaims map[string]interface{} + Claims map[string]interface{} `json:"-"` } -func (j *jwtProfileAssertion) MarshalJSON() ([]byte, error) { - type Alias jwtProfileAssertion - a := (*Alias)(j) +type jpaAlias JWTProfileAssertionClaims - b, err := json.Marshal(a) - if err != nil { - return nil, err - } - - if len(j.customClaims) == 0 { - return b, nil - } - - err = json.Unmarshal(b, &j.customClaims) - if err != nil { - return nil, fmt.Errorf("jws: invalid map of custom claims %v", j.customClaims) - } - - return json.Marshal(j.customClaims) +func (j *JWTProfileAssertionClaims) MarshalJSON() ([]byte, error) { + return mergeAndMarshalClaims((*jpaAlias)(j), j.Claims) } -func (j *jwtProfileAssertion) UnmarshalJSON(data []byte) error { - type Alias jwtProfileAssertion - a := (*Alias)(j) - - err := json.Unmarshal(data, a) - if err != nil { - return err - } - - err = json.Unmarshal(data, &j.customClaims) - if err != nil { - return err - } - - return nil +func (j *JWTProfileAssertionClaims) UnmarshalJSON(data []byte) error { + return unmarshalJSONMulti(data, (*jpaAlias)(j), &j.Claims) } -func (j *jwtProfileAssertion) GetKeyID() string { - return j.PrivateKeyID -} - -func (j *jwtProfileAssertion) GetPrivateKey() []byte { - return j.PrivateKey -} - -func (j *jwtProfileAssertion) SetCustomClaim(key string, value interface{}) { - if j.customClaims == nil { - j.customClaims = make(map[string]interface{}) - } - j.customClaims[key] = value -} - -func (j *jwtProfileAssertion) GetCustomClaim(key string) interface{} { - if j.customClaims == nil { - return nil - } - return j.customClaims[key] -} - -func (j *jwtProfileAssertion) GetIssuer() string { - return j.Issuer -} - -func (j *jwtProfileAssertion) GetSubject() string { - return j.Subject -} - -func (j *jwtProfileAssertion) GetAudience() []string { - return j.Audience -} - -func (j *jwtProfileAssertion) GetExpiration() time.Time { - return time.Time(j.Expiration) -} - -func (j *jwtProfileAssertion) GetIssuedAt() time.Time { - return time.Time(j.IssuedAt) -} - -func NewJWTProfileAssertionFromKeyJSON(filename string, audience []string, opts ...AssertionOption) (JWTProfileAssertionClaims, error) { - data, err := ioutil.ReadFile(filename) +func NewJWTProfileAssertionFromKeyJSON(filename string, audience []string, opts ...AssertionOption) (*JWTProfileAssertionClaims, error) { + data, err := os.ReadFile(filename) if err != nil { return nil, err } @@ -530,19 +240,19 @@ func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string, op return GenerateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...)) } -func JWTProfileDelegatedSubject(sub string) func(*jwtProfileAssertion) { - return func(j *jwtProfileAssertion) { +func JWTProfileDelegatedSubject(sub string) func(*JWTProfileAssertionClaims) { + return func(j *JWTProfileAssertionClaims) { j.Subject = sub } } -func JWTProfileCustomClaim(key string, value interface{}) func(*jwtProfileAssertion) { - return func(j *jwtProfileAssertion) { - j.customClaims[key] = value +func JWTProfileCustomClaim(key string, value interface{}) func(*JWTProfileAssertionClaims) { + return func(j *JWTProfileAssertionClaims) { + j.Claims[key] = value } } -func NewJWTProfileAssertionFromFileData(data []byte, audience []string, opts ...AssertionOption) (JWTProfileAssertionClaims, error) { +func NewJWTProfileAssertionFromFileData(data []byte, audience []string, opts ...AssertionOption) (*JWTProfileAssertionClaims, error) { keyData := new(struct { KeyID string `json:"keyId"` Key string `json:"key"` @@ -555,18 +265,18 @@ func NewJWTProfileAssertionFromFileData(data []byte, audience []string, opts ... return NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...), nil } -type AssertionOption func(*jwtProfileAssertion) +type AssertionOption func(*JWTProfileAssertionClaims) -func NewJWTProfileAssertion(userID, keyID string, audience []string, key []byte, opts ...AssertionOption) JWTProfileAssertionClaims { - j := &jwtProfileAssertion{ +func NewJWTProfileAssertion(userID, keyID string, audience []string, key []byte, opts ...AssertionOption) *JWTProfileAssertionClaims { + j := &JWTProfileAssertionClaims{ PrivateKey: key, PrivateKeyID: keyID, Issuer: userID, Subject: userID, - IssuedAt: Time(time.Now().UTC()), - Expiration: Time(time.Now().Add(1 * time.Hour).UTC()), + IssuedAt: FromTime(time.Now().UTC()), + Expiration: FromTime(time.Now().Add(1 * time.Hour).UTC()), Audience: audience, - customClaims: make(map[string]interface{}), + Claims: make(map[string]interface{}), } for _, opt := range opts { @@ -594,14 +304,14 @@ func AppendClientIDToAudience(clientID string, audience []string) []string { return append(audience, clientID) } -func GenerateJWTProfileToken(assertion JWTProfileAssertionClaims) (string, error) { - privateKey, err := crypto.BytesToPrivateKey(assertion.GetPrivateKey()) +func GenerateJWTProfileToken(assertion *JWTProfileAssertionClaims) (string, error) { + privateKey, err := crypto.BytesToPrivateKey(assertion.PrivateKey) if err != nil { return "", err } key := jose.SigningKey{ Algorithm: jose.RS256, - Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.GetKeyID()}, + Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID}, } signer, err := jose.NewSigner(key, &jose.SignerOptions{}) if err != nil { diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index 78bd658..e63e0e5 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -187,12 +187,12 @@ func (j *JWTTokenRequest) GetAudience() []string { // GetExpiration implements the Claims interface func (j *JWTTokenRequest) GetExpiration() time.Time { - return time.Time(j.ExpiresAt) + return j.ExpiresAt.AsTime() } // GetIssuedAt implements the Claims interface func (j *JWTTokenRequest) GetIssuedAt() time.Time { - return time.Time(j.IssuedAt) + return j.ExpiresAt.AsTime() } // GetNonce implements the Claims interface diff --git a/pkg/oidc/token_test.go b/pkg/oidc/token_test.go new file mode 100644 index 0000000..0d9874e --- /dev/null +++ b/pkg/oidc/token_test.go @@ -0,0 +1,227 @@ +package oidc + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + "gopkg.in/square/go-jose.v2" +) + +var ( + tokenClaimsData = TokenClaims{ + Issuer: "zitadel", + Subject: "hello@me.com", + Audience: Audience{"foo", "bar"}, + Expiration: 12345, + IssuedAt: 12000, + JWTID: "900", + AuthorizedParty: "just@me.com", + Nonce: "6969", + AuthTime: 12000, + NotBefore: 12000, + AuthenticationContextClassReference: "something", + AuthenticationMethodsReferences: []string{"some", "methods"}, + ClientID: "777", + SignatureAlg: jose.ES256, + } + accessTokenData = &AccessTokenClaims{ + TokenClaims: tokenClaimsData, + Scopes: []string{"email", "phone"}, + Claims: map[string]interface{}{ + "foo": "bar", + }, + } + idTokenData = &IDTokenClaims{ + TokenClaims: tokenClaimsData, + NotBefore: 12000, + AccessTokenHash: "acthashhash", + CodeHash: "hashhash", + SessionID: "666", + UserInfoProfile: userInfoData.UserInfoProfile, + UserInfoEmail: userInfoData.UserInfoEmail, + UserInfoPhone: userInfoData.UserInfoPhone, + Address: userInfoData.Address, + Claims: map[string]interface{}{ + "foo": "bar", + }, + } + introspectionResponseData = &IntrospectionResponse{ + Active: true, + Scope: SpaceDelimitedArray{"email", "phone"}, + ClientID: "777", + TokenType: "idtoken", + Expiration: 12345, + IssuedAt: 12000, + NotBefore: 12000, + Subject: "hello@me.com", + Audience: Audience{"foo", "bar"}, + Issuer: "zitadel", + JWTID: "900", + Username: "muhlemmer", + UserInfoProfile: userInfoData.UserInfoProfile, + UserInfoEmail: userInfoData.UserInfoEmail, + UserInfoPhone: userInfoData.UserInfoPhone, + Address: userInfoData.Address, + Claims: map[string]interface{}{ + "foo": "bar", + }, + } + userInfoData = &UserInfo{ + Subject: "hello@me.com", + UserInfoProfile: UserInfoProfile{ + Name: "Tim Möhlmann", + GivenName: "Tim", + FamilyName: "Möhlmann", + MiddleName: "Danger", + Nickname: "muhlemmer", + Profile: "https://github.com/muhlemmer", + Picture: "https://avatars.githubusercontent.com/u/5411563?v=4", + Website: "https://zitadel.com", + Gender: "male", + Birthdate: "1st of April", + Zoneinfo: "Europe/Amsterdam", + Locale: NewLocale(language.Dutch), + UpdatedAt: 1, + PreferredUsername: "muhlemmer", + }, + UserInfoEmail: UserInfoEmail{ + Email: "tim@zitadel.com", + EmailVerified: true, + }, + UserInfoPhone: UserInfoPhone{ + PhoneNumber: "+1234567890", + PhoneNumberVerified: true, + }, + Address: &UserInfoAddress{ + Formatted: "Sesame street 666\n666-666, Smallvile\nMoon", + StreetAddress: "Sesame street 666", + Locality: "Smallvile", + Region: "Outer space", + PostalCode: "666-666", + Country: "Moon", + }, + Claims: map[string]interface{}{ + "foo": "bar", + }, + } + jwtProfileAssertionData = &JWTProfileAssertionClaims{ + PrivateKeyID: "8888", + PrivateKey: []byte("qwerty"), + Issuer: "zitadel", + Subject: "hello@me.com", + Audience: Audience{"foo", "bar"}, + Expiration: 12345, + IssuedAt: 12000, + Claims: map[string]interface{}{ + "foo": "bar", + }, + } +) + +func TestTokenClaims(t *testing.T) { + claims := tokenClaimsData + + assert.Equal(t, claims.Issuer, tokenClaimsData.GetIssuer()) + assert.Equal(t, claims.Subject, tokenClaimsData.GetSubject()) + assert.Equal(t, []string(claims.Audience), tokenClaimsData.GetAudience()) + assert.Equal(t, claims.Expiration.AsTime(), tokenClaimsData.GetExpiration()) + assert.Equal(t, claims.IssuedAt.AsTime(), tokenClaimsData.GetIssuedAt()) + assert.Equal(t, claims.Nonce, tokenClaimsData.GetNonce()) + assert.Equal(t, claims.AuthTime.AsTime(), tokenClaimsData.GetAuthTime()) + assert.Equal(t, claims.AuthorizedParty, tokenClaimsData.GetAuthorizedParty()) + assert.Equal(t, claims.SignatureAlg, tokenClaimsData.GetSignatureAlgorithm()) + assert.Equal(t, claims.AuthenticationContextClassReference, tokenClaimsData.GetAuthenticationContextClassReference()) + + claims.SetSignatureAlgorithm(jose.ES384) + assert.Equal(t, jose.ES384, claims.SignatureAlg) +} + +func TestNewAccessTokenClaims(t *testing.T) { + want := &AccessTokenClaims{ + TokenClaims: TokenClaims{ + Issuer: "zitadel", + Subject: "hello@me.com", + Audience: Audience{"foo"}, + Expiration: 12345, + JWTID: "900", + }, + } + + got := NewAccessTokenClaims( + want.Issuer, want.Subject, nil, + want.Expiration.AsTime(), want.JWTID, "foo", time.Second, + ) + + // test if the dynamic timestamps are around now, + // allowing for a delta of 1, just in case we flip on + // either side of a second boundry. + nowMinusSkew := NowTime() - 1 + assert.InDelta(t, int64(nowMinusSkew), int64(got.IssuedAt), 1) + assert.InDelta(t, int64(nowMinusSkew), int64(got.NotBefore), 1) + + // Make equal not fail on dynamic timestamp + got.IssuedAt = 0 + got.NotBefore = 0 + + assert.Equal(t, want, got) +} + +func TestIDTokenClaims_GetAccessTokenHash(t *testing.T) { + assert.Equal(t, idTokenData.AccessTokenHash, idTokenData.GetAccessTokenHash()) +} + +func TestIDTokenClaims_SetUserInfo(t *testing.T) { + want := IDTokenClaims{ + TokenClaims: TokenClaims{ + Subject: userInfoData.Subject, + }, + UserInfoProfile: userInfoData.UserInfoProfile, + UserInfoEmail: userInfoData.UserInfoEmail, + UserInfoPhone: userInfoData.UserInfoPhone, + Address: userInfoData.Address, + } + + var got IDTokenClaims + got.SetUserInfo(userInfoData) + + assert.Equal(t, want, got) +} + +func TestNewIDTokenClaims(t *testing.T) { + want := &IDTokenClaims{ + TokenClaims: TokenClaims{ + Issuer: "zitadel", + Subject: "hello@me.com", + Audience: Audience{"foo", "just@me.com"}, + Expiration: 12345, + AuthTime: 12000, + Nonce: "6969", + AuthenticationContextClassReference: "something", + AuthenticationMethodsReferences: []string{"some", "methods"}, + AuthorizedParty: "just@me.com", + ClientID: "just@me.com", + }, + } + + got := NewIDTokenClaims( + want.Issuer, want.Subject, want.Audience, + want.Expiration.AsTime(), + want.AuthTime.AsTime().Add(time.Second), + want.Nonce, want.AuthenticationContextClassReference, + want.AuthenticationMethodsReferences, want.AuthorizedParty, + time.Second, + ) + + // test if the dynamic timestamp is around now, + // allowing for a delta of 1, just in case we flip on + // either side of a second boundry. + nowMinusSkew := NowTime() - 1 + assert.InDelta(t, int64(nowMinusSkew), int64(got.IssuedAt), 1) + + // Make equal not fail on dynamic timestamp + got.IssuedAt = 0 + + assert.Equal(t, want, got) +} diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index 21b6fba..415ab04 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -46,6 +46,39 @@ func (d *Display) UnmarshalText(text []byte) error { type Gender string +type Locale struct { + tag language.Tag +} + +func NewLocale(tag language.Tag) *Locale { + return &Locale{tag: tag} +} + +func (l *Locale) Tag() language.Tag { + if l == nil { + return language.Und + } + + return l.tag +} + +func (l *Locale) String() string { + return l.Tag().String() +} + +func (l *Locale) MarshalJSON() ([]byte, error) { + tag := l.Tag() + if tag.IsRoot() { + return []byte("null"), nil + } + + return json.Marshal(tag) +} + +func (l *Locale) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &l.tag) +} + type Locales []language.Tag func (l *Locales) UnmarshalText(text []byte) error { @@ -137,19 +170,18 @@ func NewEncoder() *schema.Encoder { return e } -type Time time.Time +type Time int64 -func (t *Time) UnmarshalJSON(data []byte) error { - var i int64 - if err := json.Unmarshal(data, &i); err != nil { - return err - } - *t = Time(time.Unix(i, 0).UTC()) - return nil +func (ts Time) AsTime() time.Time { + return time.Unix(int64(ts), 0) } -func (t *Time) MarshalJSON() ([]byte, error) { - return json.Marshal(time.Time(*t).UTC().Unix()) +func FromTime(tt time.Time) Time { + return Time(tt.Unix()) +} + +func NowTime() Time { + return FromTime(time.Now()) } type RequestObject struct { @@ -162,5 +194,4 @@ func (r *RequestObject) GetIssuer() string { return r.Issuer } -func (r *RequestObject) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) { -} +func (*RequestObject) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) {} diff --git a/pkg/oidc/types_test.go b/pkg/oidc/types_test.go index 74323da..c8e2801 100644 --- a/pkg/oidc/types_test.go +++ b/pkg/oidc/types_test.go @@ -10,6 +10,7 @@ import ( "github.com/gorilla/schema" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "golang.org/x/text/language" ) @@ -111,6 +112,117 @@ func TestDisplay_UnmarshalText(t *testing.T) { } } +func TestLocale_Tag(t *testing.T) { + tests := []struct { + name string + l *Locale + want language.Tag + }{ + { + name: "nil", + l: nil, + want: language.Und, + }, + { + name: "Und", + l: NewLocale(language.Und), + want: language.Und, + }, + { + name: "language", + l: NewLocale(language.Afrikaans), + want: language.Afrikaans, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.l.Tag()) + }) + } +} + +func TestLocale_String(t *testing.T) { + tests := []struct { + name string + l *Locale + want language.Tag + }{ + { + name: "nil", + l: nil, + want: language.Und, + }, + { + name: "Und", + l: NewLocale(language.Und), + want: language.Und, + }, + { + name: "language", + l: NewLocale(language.Afrikaans), + want: language.Afrikaans, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want.String(), tt.l.String()) + }) + } +} + +func TestLocale_MarshalJSON(t *testing.T) { + tests := []struct { + name string + l *Locale + want string + wantErr bool + }{ + { + name: "nil", + l: nil, + want: "null", + }, + { + name: "und", + l: NewLocale(language.Und), + want: "null", + }, + { + name: "language", + l: NewLocale(language.Afrikaans), + want: `"af"`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := json.Marshal(tt.l) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.want, string(got)) + }) + } +} + +func TestLocale_UnmarshalJSON(t *testing.T) { + type a struct { + Locale *Locale `json:"locale,omitempty"` + } + want := a{ + Locale: NewLocale(language.Afrikaans), + } + + const input = `{"locale": "af"}` + var got a + + require.NoError(t, + json.Unmarshal([]byte(input), &got), + ) + assert.Equal(t, want, got) +} + func TestLocales_UnmarshalText(t *testing.T) { type args struct { text []byte diff --git a/pkg/oidc/userinfo.go b/pkg/oidc/userinfo.go index c8e34d6..caff58e 100644 --- a/pkg/oidc/userinfo.go +++ b/pkg/oidc/userinfo.go @@ -1,320 +1,73 @@ package oidc -import ( - "encoding/json" - "fmt" - "time" - - "golang.org/x/text/language" -) - -type UserInfo interface { - GetSubject() string +// UserInfo implements OpenID Connect Core 1.0, section 5.1. +// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims. +type UserInfo struct { + Subject string `json:"sub,omitempty"` UserInfoProfile UserInfoEmail UserInfoPhone - GetAddress() UserInfoAddress - GetClaim(key string) interface{} - GetClaims() map[string]interface{} + Address *UserInfoAddress `json:"address,omitempty"` + + Claims map[string]any `json:"-"` } -type UserInfoProfile interface { - GetName() string - GetGivenName() string - GetFamilyName() string - GetMiddleName() string - GetNickname() string - GetProfile() string - GetPicture() string - GetWebsite() string - GetGender() Gender - GetBirthdate() string - GetZoneinfo() string - GetLocale() language.Tag - GetPreferredUsername() string +func (u *UserInfo) AppendClaims(k string, v any) { + if u.Claims == nil { + u.Claims = make(map[string]any) + } + + u.Claims[k] = v } -type UserInfoEmail interface { - GetEmail() string - IsEmailVerified() bool -} - -type UserInfoPhone interface { - GetPhoneNumber() string - IsPhoneNumberVerified() bool -} - -type UserInfoAddress interface { - GetFormatted() string - GetStreetAddress() string - GetLocality() string - GetRegion() string - GetPostalCode() string - GetCountry() string -} - -type UserInfoSetter interface { - UserInfo - SetSubject(sub string) - UserInfoProfileSetter - SetEmail(email string, verified bool) - SetPhone(phone string, verified bool) - SetAddress(address UserInfoAddress) - AppendClaims(key string, values interface{}) -} - -type UserInfoProfileSetter interface { - SetName(name string) - SetGivenName(name string) - SetFamilyName(name string) - SetMiddleName(name string) - SetNickname(name string) - SetUpdatedAt(date time.Time) - SetProfile(profile string) - SetPicture(profile string) - SetWebsite(website string) - SetGender(gender Gender) - SetBirthdate(birthdate string) - SetZoneinfo(zoneInfo string) - SetLocale(locale language.Tag) - SetPreferredUsername(name string) -} - -func NewUserInfo() UserInfoSetter { - return &userinfo{} -} - -type userinfo struct { - Subject string `json:"sub,omitempty"` - userInfoProfile - userInfoEmail - userInfoPhone - Address UserInfoAddress `json:"address,omitempty"` - - claims map[string]interface{} -} - -func (u *userinfo) GetSubject() string { - return u.Subject -} - -func (u *userinfo) GetName() string { - return u.Name -} - -func (u *userinfo) GetGivenName() string { - return u.GivenName -} - -func (u *userinfo) GetFamilyName() string { - return u.FamilyName -} - -func (u *userinfo) GetMiddleName() string { - return u.MiddleName -} - -func (u *userinfo) GetNickname() string { - return u.Nickname -} - -func (u *userinfo) GetProfile() string { - return u.Profile -} - -func (u *userinfo) GetPicture() string { - return u.Picture -} - -func (u *userinfo) GetWebsite() string { - return u.Website -} - -func (u *userinfo) GetGender() Gender { - return u.Gender -} - -func (u *userinfo) GetBirthdate() string { - return u.Birthdate -} - -func (u *userinfo) GetZoneinfo() string { - return u.Zoneinfo -} - -func (u *userinfo) GetLocale() language.Tag { - return u.Locale -} - -func (u *userinfo) GetPreferredUsername() string { - return u.PreferredUsername -} - -func (u *userinfo) GetEmail() string { - return u.Email -} - -func (u *userinfo) IsEmailVerified() bool { - return bool(u.EmailVerified) -} - -func (u *userinfo) GetPhoneNumber() string { - return u.PhoneNumber -} - -func (u *userinfo) IsPhoneNumberVerified() bool { - return u.PhoneNumberVerified -} - -func (u *userinfo) GetAddress() UserInfoAddress { +// GetAddress is a safe getter that takes +// care of a possible nil value. +func (u *UserInfo) GetAddress() *UserInfoAddress { if u.Address == nil { - return &userInfoAddress{} + return new(UserInfoAddress) } return u.Address } -func (u *userinfo) GetClaim(key string) interface{} { - return u.claims[key] +type uiAlias UserInfo + +func (u *UserInfo) MarshalJSON() ([]byte, error) { + return mergeAndMarshalClaims((*uiAlias)(u), u.Claims) } -func (u *userinfo) GetClaims() map[string]interface{} { - return u.claims +func (u *UserInfo) UnmarshalJSON(data []byte) error { + return unmarshalJSONMulti(data, (*uiAlias)(u), &u.Claims) } -func (u *userinfo) SetSubject(sub string) { - u.Subject = sub +type UserInfoProfile struct { + Name string `json:"name,omitempty"` + GivenName string `json:"given_name,omitempty"` + FamilyName string `json:"family_name,omitempty"` + MiddleName string `json:"middle_name,omitempty"` + Nickname string `json:"nickname,omitempty"` + Profile string `json:"profile,omitempty"` + Picture string `json:"picture,omitempty"` + Website string `json:"website,omitempty"` + Gender Gender `json:"gender,omitempty"` + Birthdate string `json:"birthdate,omitempty"` + Zoneinfo string `json:"zoneinfo,omitempty"` + Locale *Locale `json:"locale,omitempty"` + UpdatedAt Time `json:"updated_at,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` } -func (u *userinfo) SetName(name string) { - u.Name = name -} - -func (u *userinfo) SetGivenName(name string) { - u.GivenName = name -} - -func (u *userinfo) SetFamilyName(name string) { - u.FamilyName = name -} - -func (u *userinfo) SetMiddleName(name string) { - u.MiddleName = name -} - -func (u *userinfo) SetNickname(name string) { - u.Nickname = name -} - -func (u *userinfo) SetUpdatedAt(date time.Time) { - u.UpdatedAt = Time(date) -} - -func (u *userinfo) SetProfile(profile string) { - u.Profile = profile -} - -func (u *userinfo) SetPicture(picture string) { - u.Picture = picture -} - -func (u *userinfo) SetWebsite(website string) { - u.Website = website -} - -func (u *userinfo) SetGender(gender Gender) { - u.Gender = gender -} - -func (u *userinfo) SetBirthdate(birthdate string) { - u.Birthdate = birthdate -} - -func (u *userinfo) SetZoneinfo(zoneInfo string) { - u.Zoneinfo = zoneInfo -} - -func (u *userinfo) SetLocale(locale language.Tag) { - u.Locale = locale -} - -func (u *userinfo) SetPreferredUsername(name string) { - u.PreferredUsername = name -} - -func (u *userinfo) SetEmail(email string, verified bool) { - u.Email = email - u.EmailVerified = boolString(verified) -} - -func (u *userinfo) SetPhone(phone string, verified bool) { - u.PhoneNumber = phone - u.PhoneNumberVerified = verified -} - -func (u *userinfo) SetAddress(address UserInfoAddress) { - u.Address = address -} - -func (u *userinfo) AppendClaims(key string, value interface{}) { - if u.claims == nil { - u.claims = make(map[string]interface{}) - } - u.claims[key] = value -} - -func (u *userInfoAddress) GetFormatted() string { - return u.Formatted -} - -func (u *userInfoAddress) GetStreetAddress() string { - return u.StreetAddress -} - -func (u *userInfoAddress) GetLocality() string { - return u.Locality -} - -func (u *userInfoAddress) GetRegion() string { - return u.Region -} - -func (u *userInfoAddress) GetPostalCode() string { - return u.PostalCode -} - -func (u *userInfoAddress) GetCountry() string { - return u.Country -} - -type userInfoProfile struct { - Name string `json:"name,omitempty"` - GivenName string `json:"given_name,omitempty"` - FamilyName string `json:"family_name,omitempty"` - MiddleName string `json:"middle_name,omitempty"` - Nickname string `json:"nickname,omitempty"` - Profile string `json:"profile,omitempty"` - Picture string `json:"picture,omitempty"` - Website string `json:"website,omitempty"` - Gender Gender `json:"gender,omitempty"` - Birthdate string `json:"birthdate,omitempty"` - Zoneinfo string `json:"zoneinfo,omitempty"` - Locale language.Tag `json:"locale,omitempty"` - UpdatedAt Time `json:"updated_at,omitempty"` - PreferredUsername string `json:"preferred_username,omitempty"` -} - -type userInfoEmail struct { +type UserInfoEmail struct { Email string `json:"email,omitempty"` // Handle providers that return email_verified as a string // https://forums.aws.amazon.com/thread.jspa?messageID=949441󧳁 // https://discuss.elastic.co/t/openid-error-after-authenticating-against-aws-cognito/206018/11 - EmailVerified boolString `json:"email_verified,omitempty"` + EmailVerified Bool `json:"email_verified,omitempty"` } -type boolString bool +type Bool bool -func (bs *boolString) UnmarshalJSON(data []byte) error { +func (bs *Bool) UnmarshalJSON(data []byte) error { if string(data) == "true" || string(data) == `"true"` { *bs = true } @@ -322,12 +75,12 @@ func (bs *boolString) UnmarshalJSON(data []byte) error { return nil } -type userInfoPhone struct { +type UserInfoPhone struct { PhoneNumber string `json:"phone_number,omitempty"` PhoneNumberVerified bool `json:"phone_number_verified,omitempty"` } -type userInfoAddress struct { +type UserInfoAddress struct { Formatted string `json:"formatted,omitempty"` StreetAddress string `json:"street_address,omitempty"` Locality string `json:"locality,omitempty"` @@ -336,76 +89,6 @@ type userInfoAddress struct { Country string `json:"country,omitempty"` } -func NewUserInfoAddress(streetAddress, locality, region, postalCode, country, formatted string) UserInfoAddress { - return &userInfoAddress{ - StreetAddress: streetAddress, - Locality: locality, - Region: region, - PostalCode: postalCode, - Country: country, - Formatted: formatted, - } -} - -func (u *userinfo) MarshalJSON() ([]byte, error) { - type Alias userinfo - a := &struct { - *Alias - Locale interface{} `json:"locale,omitempty"` - UpdatedAt int64 `json:"updated_at,omitempty"` - }{ - Alias: (*Alias)(u), - } - if !u.Locale.IsRoot() { - a.Locale = u.Locale - } - if !time.Time(u.UpdatedAt).IsZero() { - a.UpdatedAt = time.Time(u.UpdatedAt).Unix() - } - - b, err := json.Marshal(a) - if err != nil { - return nil, err - } - - if len(u.claims) == 0 { - return b, nil - } - - err = json.Unmarshal(b, &u.claims) - if err != nil { - return nil, fmt.Errorf("jws: invalid map of custom claims %v", u.claims) - } - - return json.Marshal(u.claims) -} - -func (u *userinfo) UnmarshalJSON(data []byte) error { - type Alias userinfo - a := &struct { - Address *userInfoAddress `json:"address,omitempty"` - *Alias - UpdatedAt int64 `json:"update_at,omitempty"` - }{ - Alias: (*Alias)(u), - } - if err := json.Unmarshal(data, &a); err != nil { - return err - } - - if a.Address != nil { - u.Address = a.Address - } - - u.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC()) - - if err := json.Unmarshal(data, &u.claims); err != nil { - return err - } - - return nil -} - type UserInfoRequest struct { AccessToken string `schema:"access_token"` } diff --git a/pkg/oidc/userinfo_test.go b/pkg/oidc/userinfo_test.go index 319a2fd..faab4e3 100644 --- a/pkg/oidc/userinfo_test.go +++ b/pkg/oidc/userinfo_test.go @@ -7,21 +7,54 @@ import ( "github.com/stretchr/testify/assert" ) +func TestUserInfo_AppendClaims(t *testing.T) { + u := new(UserInfo) + u.AppendClaims("a", "b") + want := map[string]any{"a": "b"} + assert.Equal(t, want, u.Claims) + + u.AppendClaims("d", "e") + want["d"] = "e" + assert.Equal(t, want, u.Claims) +} + +func TestUserInfo_GetAddress(t *testing.T) { + // nil address + u := new(UserInfo) + assert.Equal(t, &UserInfoAddress{}, u.GetAddress()) + + u.Address = &UserInfoAddress{PostalCode: "1234"} + assert.Equal(t, u.Address, u.GetAddress()) +} + func TestUserInfoMarshal(t *testing.T) { - userinfo := NewUserInfo() - userinfo.SetSubject("test") - userinfo.SetAddress(NewUserInfoAddress("Test 789\nPostfach 2", "", "", "", "", "")) - userinfo.SetEmail("test", true) - userinfo.SetPhone("0791234567", true) - userinfo.SetName("Test") - userinfo.AppendClaims("private_claim", "test") + userinfo := &UserInfo{ + Subject: "test", + Address: &UserInfoAddress{ + StreetAddress: "Test 789\nPostfach 2", + }, + UserInfoEmail: UserInfoEmail{ + Email: "test", + EmailVerified: true, + }, + UserInfoPhone: UserInfoPhone{ + PhoneNumber: "0791234567", + PhoneNumberVerified: true, + }, + UserInfoProfile: UserInfoProfile{ + Name: "Test", + }, + Claims: map[string]any{"private_claim": "test"}, + } marshal, err := json.Marshal(userinfo) - out := NewUserInfo() assert.NoError(t, err) + + out := new(UserInfo) assert.NoError(t, json.Unmarshal(marshal, out)) - assert.Equal(t, userinfo.GetAddress(), out.GetAddress()) + assert.Equal(t, userinfo, out) expected, err := json.Marshal(out) + assert.NoError(t, err) assert.Equal(t, expected, marshal) } @@ -29,91 +62,55 @@ func TestUserInfoMarshal(t *testing.T) { func TestUserInfoEmailVerifiedUnmarshal(t *testing.T) { t.Parallel() - t.Run("unmarsha email_verified from json bool true", func(t *testing.T) { + t.Run("unmarshal email_verified from json bool true", func(t *testing.T) { jsonBool := []byte(`{"email": "my@email.com", "email_verified": true}`) - var uie userInfoEmail + var uie UserInfoEmail err := json.Unmarshal(jsonBool, &uie) assert.NoError(t, err) - assert.Equal(t, userInfoEmail{ + assert.Equal(t, UserInfoEmail{ Email: "my@email.com", EmailVerified: true, }, uie) }) - t.Run("unmarsha email_verified from json string true", func(t *testing.T) { + t.Run("unmarshal email_verified from json string true", func(t *testing.T) { jsonBool := []byte(`{"email": "my@email.com", "email_verified": "true"}`) - var uie userInfoEmail + var uie UserInfoEmail err := json.Unmarshal(jsonBool, &uie) assert.NoError(t, err) - assert.Equal(t, userInfoEmail{ + assert.Equal(t, UserInfoEmail{ Email: "my@email.com", EmailVerified: true, }, uie) }) - t.Run("unmarsha email_verified from json bool false", func(t *testing.T) { + t.Run("unmarshal email_verified from json bool false", func(t *testing.T) { jsonBool := []byte(`{"email": "my@email.com", "email_verified": false}`) - var uie userInfoEmail + var uie UserInfoEmail err := json.Unmarshal(jsonBool, &uie) assert.NoError(t, err) - assert.Equal(t, userInfoEmail{ + assert.Equal(t, UserInfoEmail{ Email: "my@email.com", EmailVerified: false, }, uie) }) - t.Run("unmarsha email_verified from json string false", func(t *testing.T) { + t.Run("unmarshal email_verified from json string false", func(t *testing.T) { jsonBool := []byte(`{"email": "my@email.com", "email_verified": "false"}`) - var uie userInfoEmail + var uie UserInfoEmail err := json.Unmarshal(jsonBool, &uie) assert.NoError(t, err) - assert.Equal(t, userInfoEmail{ + assert.Equal(t, UserInfoEmail{ Email: "my@email.com", EmailVerified: false, }, uie) }) } - -// issue 203 test case. -func Test_userinfo_GetAddress_issue_203(t *testing.T) { - tests := []struct { - name string - data string - }{ - { - name: "with address", - data: `{"address":{"street_address":"Test 789\nPostfach 2"},"email":"test","email_verified":true,"name":"Test","phone_number":"0791234567","phone_number_verified":true,"private_claim":"test","sub":"test"}`, - }, - { - name: "without address", - data: `{"email":"test","email_verified":true,"name":"Test","phone_number":"0791234567","phone_number_verified":true,"private_claim":"test","sub":"test"}`, - }, - { - name: "null address", - data: `{"address":null,"email":"test","email_verified":true,"name":"Test","phone_number":"0791234567","phone_number_verified":true,"private_claim":"test","sub":"test"}`, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - info := &userinfo{} - err := json.Unmarshal([]byte(tt.data), info) - assert.NoError(t, err) - - info.GetAddress().GetCountry() //<- used to panic - - // now shortly assure that a marshalling still produces the same as was parsed into the struct - marshal, err := json.Marshal(info) - assert.NoError(t, err) - assert.Equal(t, tt.data, string(marshal)) - }) - } -} diff --git a/pkg/oidc/util.go b/pkg/oidc/util.go new file mode 100644 index 0000000..a89d75e --- /dev/null +++ b/pkg/oidc/util.go @@ -0,0 +1,49 @@ +package oidc + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// mergeAndMarshalClaims merges registered and the custom +// claims map into a single JSON object. +// Registered fields overwrite custom claims. +func mergeAndMarshalClaims(registered any, claims map[string]any) ([]byte, error) { + // Use a buffer for memory re-use, instead off letting + // json allocate a new []byte for every step. + buf := new(bytes.Buffer) + + // Marshal the registered claims into JSON + if err := json.NewEncoder(buf).Encode(registered); err != nil { + return nil, fmt.Errorf("oidc registered claims: %w", err) + } + + if len(claims) > 0 { + // Merge JSON data into custom claims. + // The full-read action by the decoder resets the buffer + // to zero len, while retaining underlaying cap. + if err := json.NewDecoder(buf).Decode(&claims); err != nil { + return nil, fmt.Errorf("oidc registered claims: %w", err) + } + + // Marshal the final result. + if err := json.NewEncoder(buf).Encode(claims); err != nil { + return nil, fmt.Errorf("oidc custom claims: %w", err) + } + } + + return buf.Bytes(), nil +} + +// unmarshalJSONMulti unmarshals the same JSON data into multiple destinations. +// Each destination must be a pointer, as per json.Unmarshal rules. +// Returns on the first error and destinations may be partly filled with data. +func unmarshalJSONMulti(data []byte, destinations ...any) error { + for _, dst := range destinations { + if err := json.Unmarshal(data, dst); err != nil { + return fmt.Errorf("oidc: %w into %T", err, dst) + } + } + return nil +} diff --git a/pkg/oidc/util_test.go b/pkg/oidc/util_test.go new file mode 100644 index 0000000..6363d83 --- /dev/null +++ b/pkg/oidc/util_test.go @@ -0,0 +1,147 @@ +package oidc + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type jsonErrorTest struct{} + +func (jsonErrorTest) MarshalJSON() ([]byte, error) { + return nil, errors.New("test") +} + +func Test_mergeAndMarshalClaims(t *testing.T) { + type args struct { + registered any + claims map[string]any + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "encoder error", + args: args{ + registered: jsonErrorTest{}, + }, + wantErr: true, + }, + { + name: "no claims", + args: args{ + registered: struct { + Foo string `json:"foo,omitempty"` + }{ + Foo: "bar", + }, + }, + want: "{\"foo\":\"bar\"}\n", + }, + { + name: "with claims", + args: args{ + registered: struct { + Foo string `json:"foo,omitempty"` + }{ + Foo: "bar", + }, + claims: map[string]any{ + "bar": "foo", + }, + }, + want: "{\"bar\":\"foo\",\"foo\":\"bar\"}\n", + }, + { + name: "registered overwrites custom", + args: args{ + registered: struct { + Foo string `json:"foo,omitempty"` + }{ + Foo: "bar", + }, + claims: map[string]any{ + "foo": "Hello, World!", + }, + }, + want: "{\"foo\":\"bar\"}\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := mergeAndMarshalClaims(tt.args.registered, tt.args.claims) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.want, string(got)) + }) + } +} + +func Test_unmarshalJSONMulti(t *testing.T) { + type dst struct { + Foo string `json:"foo,omitempty"` + } + + type args struct { + data string + destinations []any + } + tests := []struct { + name string + args args + want []any + wantErr bool + }{ + { + name: "error", + args: args{ + data: "~!~~", + destinations: []any{ + &dst{}, + &map[string]any{}, + }, + }, + want: []any{ + &dst{}, + &map[string]any{}, + }, + wantErr: true, + }, + { + name: "success", + args: args{ + data: "{\"bar\":\"foo\",\"foo\":\"bar\"}\n", + destinations: []any{ + &dst{}, + &map[string]any{}, + }, + }, + want: []any{ + &dst{Foo: "bar"}, + &map[string]any{ + "foo": "bar", + "bar": "foo", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := unmarshalJSONMulti([]byte(tt.args.data), tt.args.destinations...) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.want, tt.args.destinations) + }) + } +} diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go index 1757651..c4ee95e 100644 --- a/pkg/oidc/verifier.go +++ b/pkg/oidc/verifier.go @@ -32,6 +32,12 @@ type ClaimsSignature interface { SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) } +type IDClaims interface { + Claims + GetSignatureAlgorithm() jose.SignatureAlgorithm + GetAccessTokenHash() string +} + var ( ErrParse = errors.New("parsing of request failed") ErrIssuerInvalid = errors.New("issuer does not match") diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index b13f642..bd6aa95 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -371,7 +371,7 @@ func ValidateAuthReqIDTokenHint(ctx context.Context, idTokenHint string, verifie if idTokenHint == "" { return "", nil } - claims, err := VerifyIDTokenHint(ctx, idTokenHint, verifier) + claims, err := VerifyIDTokenHint[*oidc.TokenClaims](ctx, idTokenHint, verifier) if err != nil { return "", oidc.ErrLoginRequired().WithDescription("The id_token_hint is invalid. " + "If you have any questions, you may contact the administrator of the application.") diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index fc0c358..85afb2a 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -263,7 +263,7 @@ func (mr *MockStorageMockRecorder) SaveAuthCode(arg0, arg1, arg2 interface{}) *g } // SetIntrospectionFromToken mocks base method. -func (m *MockStorage) SetIntrospectionFromToken(arg0 context.Context, arg1 oidc.IntrospectionResponse, arg2, arg3, arg4 string) error { +func (m *MockStorage) SetIntrospectionFromToken(arg0 context.Context, arg1 *oidc.IntrospectionResponse, arg2, arg3, arg4 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetIntrospectionFromToken", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(error) @@ -277,7 +277,7 @@ func (mr *MockStorageMockRecorder) SetIntrospectionFromToken(arg0, arg1, arg2, a } // SetUserinfoFromScopes mocks base method. -func (m *MockStorage) SetUserinfoFromScopes(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3 string, arg4 []string) error { +func (m *MockStorage) SetUserinfoFromScopes(arg0 context.Context, arg1 *oidc.UserInfo, arg2, arg3 string, arg4 []string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetUserinfoFromScopes", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(error) @@ -291,7 +291,7 @@ func (mr *MockStorageMockRecorder) SetUserinfoFromScopes(arg0, arg1, arg2, arg3, } // SetUserinfoFromToken mocks base method. -func (m *MockStorage) SetUserinfoFromToken(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3, arg4 string) error { +func (m *MockStorage) SetUserinfoFromToken(arg0 context.Context, arg1 *oidc.UserInfo, arg2, arg3, arg4 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetUserinfoFromToken", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(error) diff --git a/pkg/op/session.go b/pkg/op/session.go index e1cc595..3e5ec3c 100644 --- a/pkg/op/session.go +++ b/pkg/op/session.go @@ -59,7 +59,7 @@ func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest, RedirectURI: ender.DefaultLogoutRedirectURI(), } if req.IdTokenHint != "" { - claims, err := VerifyIDTokenHint(ctx, req.IdTokenHint, ender.IDTokenHintVerifier(ctx)) + claims, err := VerifyIDTokenHint[*oidc.TokenClaims](ctx, req.IdTokenHint, ender.IDTokenHintVerifier(ctx)) if err != nil { return nil, oidc.ErrInvalidRequest().WithDescription("id_token_hint invalid").WithParent(err) } diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 8ba1946..5940bd9 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -96,7 +96,7 @@ type TokenExchangeStorage interface { // SetUserinfoFromTokenExchangeRequest will be called during id token creation. // Claims evaluation can be based on all validated request data available, including: scopes, resource, audience, etc. - SetUserinfoFromTokenExchangeRequest(ctx context.Context, userinfo oidc.UserInfoSetter, request TokenExchangeRequest) error + SetUserinfoFromTokenExchangeRequest(ctx context.Context, userinfo *oidc.UserInfo, request TokenExchangeRequest) error } // TokenExchangeTokensVerifierStorage is an optional interface used in token exchange process to verify tokens @@ -111,9 +111,9 @@ var ErrInvalidRefreshToken = errors.New("invalid_refresh_token") type OPStorage interface { GetClientByClientID(ctx context.Context, clientID string) (Client, error) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error - SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error - SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error - SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, clientID string) error + SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error + SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, subject, origin string) error + SetIntrospectionFromToken(ctx context.Context, userinfo *oidc.IntrospectionResponse, tokenID, subject, clientID string) error GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) GetKeyByIDAndClientID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error) ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error) diff --git a/pkg/op/token.go b/pkg/op/token.go index 3a35062..58568a7 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -129,7 +129,7 @@ func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, ex if err != nil { return "", err } - claims.SetPrivateClaims(privateClaims) + claims.Claims = privateClaims } signingKey, err := storage.SigningKey(ctx) if err != nil { @@ -169,7 +169,7 @@ func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, v if err != nil { return "", err } - claims.SetAccessTokenHash(atHash) + claims.AccessTokenHash = atHash if !client.IDTokenUserinfoClaimsAssertion() { scopes = removeUserinfoScopes(scopes) } @@ -178,26 +178,26 @@ func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, v tokenExchangeRequest, okReq := request.(TokenExchangeRequest) teStorage, okStorage := storage.(TokenExchangeStorage) if okReq && okStorage { - userInfo := oidc.NewUserInfo() + userInfo := new(oidc.UserInfo) err := teStorage.SetUserinfoFromTokenExchangeRequest(ctx, userInfo, tokenExchangeRequest) if err != nil { return "", err } - claims.SetUserinfo(userInfo) + claims.SetUserInfo(userInfo) } else if len(scopes) > 0 { - userInfo := oidc.NewUserInfo() + userInfo := new(oidc.UserInfo) err := storage.SetUserinfoFromScopes(ctx, userInfo, request.GetSubject(), request.GetClientID(), scopes) if err != nil { return "", err } - claims.SetUserinfo(userInfo) + claims.SetUserInfo(userInfo) } if code != "" { codeHash, err := oidc.ClaimHash(code, signingKey.SignatureAlgorithm()) if err != nil { return "", err } - claims.SetCodeHash(codeHash) + claims.CodeHash = codeHash } signer, err := SignerFromKey(signingKey) if err != nil { diff --git a/pkg/op/token_exchange.go b/pkg/op/token_exchange.go index 6b918b1..055ff13 100644 --- a/pkg/op/token_exchange.go +++ b/pkg/op/token_exchange.go @@ -280,9 +280,9 @@ func GetTokenIDAndSubjectFromToken( ) (tokenIDOrToken, subject string, claims map[string]interface{}, ok bool) { switch tokenType { case oidc.AccessTokenType: - var accessTokenClaims oidc.AccessTokenClaims + var accessTokenClaims *oidc.AccessTokenClaims tokenIDOrToken, subject, accessTokenClaims, ok = getTokenIDAndClaims(ctx, exchanger, token) - claims = accessTokenClaims.GetClaims() + claims = accessTokenClaims.Claims case oidc.RefreshTokenType: refreshTokenRequest, err := exchanger.Storage().TokenRequestByRefreshToken(ctx, token) if err != nil { @@ -291,12 +291,12 @@ func GetTokenIDAndSubjectFromToken( tokenIDOrToken, subject, ok = token, refreshTokenRequest.GetSubject(), true case oidc.IDTokenType: - idTokenClaims, err := VerifyIDTokenHint(ctx, token, exchanger.IDTokenHintVerifier(ctx)) + idTokenClaims, err := VerifyIDTokenHint[*oidc.IDTokenClaims](ctx, token, exchanger.IDTokenHintVerifier(ctx)) if err != nil { break } - tokenIDOrToken, subject, claims, ok = token, idTokenClaims.GetSubject(), idTokenClaims.GetClaims(), true + tokenIDOrToken, subject, claims, ok = token, idTokenClaims.Subject, idTokenClaims.Claims, true } if !ok { @@ -380,7 +380,7 @@ func CreateTokenExchangeResponse( }, nil } -func getTokenIDAndClaims(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, oidc.AccessTokenClaims, bool) { +func getTokenIDAndClaims(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, *oidc.AccessTokenClaims, bool) { tokenIDSubject, err := userinfoProvider.Crypto().Decrypt(accessToken) if err == nil { splitToken := strings.Split(tokenIDSubject, ":") @@ -390,10 +390,10 @@ func getTokenIDAndClaims(ctx context.Context, userinfoProvider UserinfoProvider, return splitToken[0], splitToken[1], nil, true } - accessTokenClaims, err := VerifyAccessToken(ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx)) + accessTokenClaims, err := VerifyAccessToken[*oidc.AccessTokenClaims](ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx)) if err != nil { return "", "", nil, false } - return accessTokenClaims.GetTokenID(), accessTokenClaims.GetSubject(), accessTokenClaims, true + return accessTokenClaims.JWTID, accessTokenClaims.Subject, accessTokenClaims, true } diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go index e7ca7c4..8582388 100644 --- a/pkg/op/token_intospection.go +++ b/pkg/op/token_intospection.go @@ -28,7 +28,7 @@ func introspectionHandler(introspector Introspector) func(http.ResponseWriter, * } func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspector) { - response := oidc.NewIntrospectionResponse() + response := new(oidc.IntrospectionResponse) token, clientID, err := ParseTokenIntrospectionRequest(r, introspector) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) @@ -44,7 +44,7 @@ func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspecto httphelper.MarshalJSON(w, response) return } - response.SetActive(true) + response.Active = true httphelper.MarshalJSON(w, response) } diff --git a/pkg/op/token_revocation.go b/pkg/op/token_revocation.go index 33978f5..58332c3 100644 --- a/pkg/op/token_revocation.go +++ b/pkg/op/token_revocation.go @@ -151,9 +151,9 @@ func getTokenIDAndSubjectForRevocation(ctx context.Context, userinfoProvider Use } return splitToken[0], splitToken[1], true } - accessTokenClaims, err := VerifyAccessToken(ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx)) + accessTokenClaims, err := VerifyAccessToken[*oidc.AccessTokenClaims](ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx)) if err != nil { return "", "", false } - return accessTokenClaims.GetTokenID(), accessTokenClaims.GetSubject(), true + return accessTokenClaims.JWTID, accessTokenClaims.Subject, true } diff --git a/pkg/op/userinfo.go b/pkg/op/userinfo.go index cb8f0ae..21a0af4 100644 --- a/pkg/op/userinfo.go +++ b/pkg/op/userinfo.go @@ -34,7 +34,7 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP http.Error(w, "access token invalid", http.StatusUnauthorized) return } - info := oidc.NewUserInfo() + info := new(oidc.UserInfo) err = userinfoProvider.Storage().SetUserinfoFromToken(r.Context(), info, tokenID, subject, r.Header.Get("origin")) if err != nil { httphelper.MarshalJSONWithStatus(w, err, http.StatusForbidden) @@ -81,9 +81,9 @@ func getTokenIDAndSubject(ctx context.Context, userinfoProvider UserinfoProvider } return splitToken[0], splitToken[1], true } - accessTokenClaims, err := VerifyAccessToken(ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx)) + accessTokenClaims, err := VerifyAccessToken[*oidc.AccessTokenClaims](ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx)) if err != nil { return "", "", false } - return accessTokenClaims.GetTokenID(), accessTokenClaims.GetSubject(), true + return accessTokenClaims.JWTID, accessTokenClaims.Subject, true } diff --git a/pkg/op/verifier_access_token.go b/pkg/op/verifier_access_token.go index 1d53adb..9a8b912 100644 --- a/pkg/op/verifier_access_token.go +++ b/pkg/op/verifier_access_token.go @@ -18,8 +18,6 @@ type accessTokenVerifier struct { maxAgeIAT time.Duration offset time.Duration supportedSignAlgs []string - maxAge time.Duration - acr oidc.ACRVerifier keySet oidc.KeySet } @@ -67,29 +65,29 @@ func NewAccessTokenVerifier(issuer string, keySet oidc.KeySet, opts ...AccessTok return verifier } -// VerifyAccessToken validates the access token (issuer, signature and expiration) -func VerifyAccessToken(ctx context.Context, token string, v AccessTokenVerifier) (oidc.AccessTokenClaims, error) { - claims := oidc.EmptyAccessTokenClaims() +// VerifyAccessToken validates the access token (issuer, signature and expiration). +func VerifyAccessToken[C oidc.Claims](ctx context.Context, token string, v AccessTokenVerifier) (claims C, err error) { + var nilClaims C decrypted, err := oidc.DecryptToken(token) if err != nil { - return nil, err + return nilClaims, err } - payload, err := oidc.ParseToken(decrypted, claims) + payload, err := oidc.ParseToken(decrypted, &claims) if err != nil { - return nil, err + return nilClaims, err } if err := oidc.CheckIssuer(claims, v.Issuer()); err != nil { - return nil, err + return nilClaims, err } if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil { - return nil, err + return nilClaims, err } if err = oidc.CheckExpiration(claims, v.Offset()); err != nil { - return nil, err + return nilClaims, err } return claims, nil diff --git a/pkg/op/verifier_access_token_example_test.go b/pkg/op/verifier_access_token_example_test.go new file mode 100644 index 0000000..effdd58 --- /dev/null +++ b/pkg/op/verifier_access_token_example_test.go @@ -0,0 +1,70 @@ +package op_test + +import ( + "context" + "fmt" + + tu "github.com/zitadel/oidc/v2/internal/testutil" + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" +) + +// MyCustomClaims extends the TokenClaims base, +// so it implements the oidc.Claims interface. +// Instead of carrying a map, we add needed fields// to the struct for type safe access. +type MyCustomClaims struct { + oidc.TokenClaims + NotBefore oidc.Time `json:"nbf,omitempty"` + CodeHash string `json:"c_hash,omitempty"` + SessionID string `json:"sid,omitempty"` + Scopes []string `json:"scope,omitempty"` + AccessTokenUseNumber int `json:"at_use_nbr,omitempty"` + Foo string `json:"foo,omitempty"` + Bar *Nested `json:"bar,omitempty"` +} + +// Nested struct types are also possible. +type Nested struct { + Count int `json:"count,omitempty"` + Tags []string `json:"tags,omitempty"` +} + +/* +accessToken carries the following claims. foo and bar are custom claims + + { + "aud": [ + "unit", + "test" + ], + "bar": { + "count": 22, + "tags": [ + "some", + "tags" + ] + }, + "exp": 4802234675, + "foo": "Hello, World!", + "iat": 1678097014, + "iss": "local.com", + "jti": "9876", + "nbf": 1678097014, + "sub": "tim@local.com" + } +*/ +const accessToken = `eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJhdWQiOlsidW5pdCIsInRlc3QiXSwiYmFyIjp7ImNvdW50IjoyMiwidGFncyI6WyJzb21lIiwidGFncyJdfSwiZXhwIjo0ODAyMjM0Njc1LCJmb28iOiJIZWxsbywgV29ybGQhIiwiaWF0IjoxNjc4MDk3MDE0LCJpc3MiOiJsb2NhbC5jb20iLCJqdGkiOiI5ODc2IiwibmJmIjoxNjc4MDk3MDE0LCJzdWIiOiJ0aW1AbG9jYWwuY29tIn0.OUgk-B7OXjYlYFj-nogqSDJiQE19tPrbzqUHEAjcEiJkaWo6-IpGVfDiGKm-TxjXQsNScxpaY0Pg3XIh1xK6TgtfYtoLQm-5RYw_mXgb9xqZB2VgPs6nNEYFUDM513MOU0EBc0QMyqAEGzW-HiSPAb4ugCvkLtM1yo11Xyy6vksAdZNs_mJDT4X3vFXnr0jk0ugnAW6fTN3_voC0F_9HQUAkmd750OIxkAHxAMvEPQcpbLHenVvX_Q0QMrzClVrxehn5TVMfmkYYg7ocr876Bq9xQGPNHAcrwvVIJqdg5uMUA38L3HC2BEueG6furZGvc7-qDWAT1VR9liM5ieKpPg` + +func ExampleVerifyAccessToken_customClaims() { + v := op.NewAccessTokenVerifier("local.com", tu.KeySet{}) + + // VerifyAccessToken can be called with the *MyCustomClaims. + claims, err := op.VerifyAccessToken[*MyCustomClaims](context.TODO(), accessToken, v) + if err != nil { + panic(err) + } + + // Here we have typesafe access to the custom claims + fmt.Println(claims.Foo, claims.Bar.Count, claims.Bar.Tags) + // Output: Hello, World! 22 [some tags] +} diff --git a/pkg/op/verifier_access_token_test.go b/pkg/op/verifier_access_token_test.go new file mode 100644 index 0000000..62c26a9 --- /dev/null +++ b/pkg/op/verifier_access_token_test.go @@ -0,0 +1,126 @@ +package op + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + tu "github.com/zitadel/oidc/v2/internal/testutil" + "github.com/zitadel/oidc/v2/pkg/oidc" +) + +func TestNewAccessTokenVerifier(t *testing.T) { + type args struct { + issuer string + keySet oidc.KeySet + opts []AccessTokenVerifierOpt + } + tests := []struct { + name string + args args + want AccessTokenVerifier + }{ + { + name: "simple", + args: args{ + issuer: tu.ValidIssuer, + keySet: tu.KeySet{}, + }, + want: &accessTokenVerifier{ + issuer: tu.ValidIssuer, + keySet: tu.KeySet{}, + }, + }, + { + name: "with signature algorithm", + args: args{ + issuer: tu.ValidIssuer, + keySet: tu.KeySet{}, + opts: []AccessTokenVerifierOpt{ + WithSupportedAccessTokenSigningAlgorithms("ABC", "DEF"), + }, + }, + want: &accessTokenVerifier{ + issuer: tu.ValidIssuer, + keySet: tu.KeySet{}, + supportedSignAlgs: []string{"ABC", "DEF"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewAccessTokenVerifier(tt.args.issuer, tt.args.keySet, tt.args.opts...) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestVerifyAccessToken(t *testing.T) { + verifier := &accessTokenVerifier{ + issuer: tu.ValidIssuer, + maxAgeIAT: 2 * time.Minute, + offset: time.Second, + supportedSignAlgs: []string{string(tu.SignatureAlgorithm)}, + keySet: tu.KeySet{}, + } + + tests := []struct { + name string + tokenClaims func() (string, *oidc.AccessTokenClaims) + wantErr bool + }{ + { + name: "success", + tokenClaims: tu.ValidAccessToken, + }, + { + name: "parse err", + tokenClaims: func() (string, *oidc.AccessTokenClaims) { return "~~~~", nil }, + wantErr: true, + }, + { + name: "invalid signature", + tokenClaims: func() (string, *oidc.AccessTokenClaims) { return tu.InvalidSignatureToken, nil }, + wantErr: true, + }, + { + name: "wrong issuer", + tokenClaims: func() (string, *oidc.AccessTokenClaims) { + return tu.NewAccessToken( + "foo", tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration, tu.ValidJWTID, tu.ValidClientID, + tu.ValidSkew, + ) + }, + wantErr: true, + }, + { + name: "expired", + tokenClaims: func() (string, *oidc.AccessTokenClaims) { + return tu.NewAccessToken( + tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration.Add(-time.Hour), tu.ValidJWTID, tu.ValidClientID, + tu.ValidSkew, + ) + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + token, want := tt.tokenClaims() + + got, err := VerifyAccessToken[*oidc.AccessTokenClaims](context.Background(), token, verifier) + if tt.wantErr { + assert.Error(t, err) + assert.Nil(t, got) + return + } + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, got, want) + }) + } +} diff --git a/pkg/op/verifier_id_token_hint.go b/pkg/op/verifier_id_token_hint.go index 9320106..d906075 100644 --- a/pkg/op/verifier_id_token_hint.go +++ b/pkg/op/verifier_id_token_hint.go @@ -74,40 +74,40 @@ func NewIDTokenHintVerifier(issuer string, keySet oidc.KeySet, opts ...IDTokenHi // VerifyIDTokenHint validates the id token according to // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation -func VerifyIDTokenHint(ctx context.Context, token string, v IDTokenHintVerifier) (oidc.IDTokenClaims, error) { - claims := oidc.EmptyIDTokenClaims() +func VerifyIDTokenHint[C oidc.Claims](ctx context.Context, token string, v IDTokenHintVerifier) (claims C, err error) { + var nilClaims C decrypted, err := oidc.DecryptToken(token) if err != nil { - return nil, err + return nilClaims, err } - payload, err := oidc.ParseToken(decrypted, claims) + payload, err := oidc.ParseToken(decrypted, &claims) if err != nil { - return nil, err + return nilClaims, err } if err := oidc.CheckIssuer(claims, v.Issuer()); err != nil { - return nil, err + return nilClaims, err } if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil { - return nil, err + return nilClaims, err } if err = oidc.CheckExpiration(claims, v.Offset()); err != nil { - return nil, err + return nilClaims, err } if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT(), v.Offset()); err != nil { - return nil, err + return nilClaims, err } if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR()); err != nil { - return nil, err + return nilClaims, err } if err = oidc.CheckAuthTime(claims, v.MaxAge()); err != nil { - return nil, err + return nilClaims, err } return claims, nil } diff --git a/pkg/op/verifier_id_token_hint_test.go b/pkg/op/verifier_id_token_hint_test.go new file mode 100644 index 0000000..f4d0b0c --- /dev/null +++ b/pkg/op/verifier_id_token_hint_test.go @@ -0,0 +1,161 @@ +package op + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + tu "github.com/zitadel/oidc/v2/internal/testutil" + "github.com/zitadel/oidc/v2/pkg/oidc" +) + +func TestNewIDTokenHintVerifier(t *testing.T) { + type args struct { + issuer string + keySet oidc.KeySet + opts []IDTokenHintVerifierOpt + } + tests := []struct { + name string + args args + want IDTokenHintVerifier + }{ + { + name: "simple", + args: args{ + issuer: tu.ValidIssuer, + keySet: tu.KeySet{}, + }, + want: &idTokenHintVerifier{ + issuer: tu.ValidIssuer, + keySet: tu.KeySet{}, + }, + }, + { + name: "with signature algorithm", + args: args{ + issuer: tu.ValidIssuer, + keySet: tu.KeySet{}, + opts: []IDTokenHintVerifierOpt{ + WithSupportedIDTokenHintSigningAlgorithms("ABC", "DEF"), + }, + }, + want: &idTokenHintVerifier{ + issuer: tu.ValidIssuer, + keySet: tu.KeySet{}, + supportedSignAlgs: []string{"ABC", "DEF"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewIDTokenHintVerifier(tt.args.issuer, tt.args.keySet, tt.args.opts...) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestVerifyIDTokenHint(t *testing.T) { + verifier := &idTokenHintVerifier{ + issuer: tu.ValidIssuer, + maxAgeIAT: 2 * time.Minute, + offset: time.Second, + supportedSignAlgs: []string{string(tu.SignatureAlgorithm)}, + maxAge: 2 * time.Minute, + acr: tu.ACRVerify, + keySet: tu.KeySet{}, + } + + tests := []struct { + name string + tokenClaims func() (string, *oidc.IDTokenClaims) + wantErr bool + }{ + { + name: "success", + tokenClaims: tu.ValidIDToken, + }, + { + name: "parse err", + tokenClaims: func() (string, *oidc.IDTokenClaims) { return "~~~~", nil }, + wantErr: true, + }, + { + name: "invalid signature", + tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.InvalidSignatureToken, nil }, + wantErr: true, + }, + { + name: "wrong issuer", + tokenClaims: func() (string, *oidc.IDTokenClaims) { + return tu.NewIDToken( + "foo", tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce, + tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "", + ) + }, + wantErr: true, + }, + { + name: "expired", + tokenClaims: func() (string, *oidc.IDTokenClaims) { + return tu.NewIDToken( + tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration.Add(-time.Hour), tu.ValidAuthTime, tu.ValidNonce, + tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "", + ) + }, + wantErr: true, + }, + { + name: "wrong IAT", + tokenClaims: func() (string, *oidc.IDTokenClaims) { + return tu.NewIDToken( + tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce, + tu.ValidACR, tu.ValidAMR, tu.ValidClientID, -time.Hour, "", + ) + }, + wantErr: true, + }, + { + name: "wrong acr", + tokenClaims: func() (string, *oidc.IDTokenClaims) { + return tu.NewIDToken( + tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce, + "else", tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "", + ) + }, + wantErr: true, + }, + { + name: "expired auth", + tokenClaims: func() (string, *oidc.IDTokenClaims) { + return tu.NewIDToken( + tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration, tu.ValidAuthTime.Add(-time.Hour), tu.ValidNonce, + tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "", + ) + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + token, want := tt.tokenClaims() + + got, err := VerifyIDTokenHint[*oidc.IDTokenClaims](context.Background(), token, verifier) + if tt.wantErr { + assert.Error(t, err) + assert.Nil(t, got) + return + } + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, got, want) + }) + } +} From 711a194b5036ce618bd6ab8ca29b5f9653c10cf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Mon, 6 Mar 2023 19:21:24 +0200 Subject: [PATCH 193/502] fix: allow RFC3339 encoded time strings Fixes #292 --- pkg/oidc/types.go | 24 +++++++++++++++++++ pkg/oidc/types_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index 415ab04..cb513a0 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -184,6 +184,30 @@ func NowTime() Time { return FromTime(time.Now()) } +func (ts *Time) UnmarshalJSON(data []byte) error { + var v any + if err := json.Unmarshal(data, &v); err != nil { + return fmt.Errorf("oidc.Time: %w", err) + } + switch x := v.(type) { + case float64: + *ts = Time(x) + case string: + // Compatibility with Auth0: + // https://github.com/zitadel/oidc/issues/292 + tt, err := time.Parse(time.RFC3339, x) + if err != nil { + return fmt.Errorf("oidc.Time: %w", err) + } + *ts = FromTime(tt) + case nil: + *ts = 0 + default: + return fmt.Errorf("oidc.Time: unable to parse type %T with value %v", x, x) + } + return nil +} + type RequestObject struct { Issuer string `json:"iss"` Audience Audience `json:"aud"` diff --git a/pkg/oidc/types_test.go b/pkg/oidc/types_test.go index c8e2801..2721e0b 100644 --- a/pkg/oidc/types_test.go +++ b/pkg/oidc/types_test.go @@ -466,3 +466,57 @@ func TestNewEncoder(t *testing.T) { schema.NewDecoder().Decode(&b, values) assert.Equal(t, a, b) } + +func TestTime_UnmarshalJSON(t *testing.T) { + type dst struct { + UpdatedAt Time `json:"updated_at"` + } + tests := []struct { + name string + json string + want dst + wantErr bool + }{ + { + name: "RFC3339", // https://github.com/zitadel/oidc/issues/292 + json: `{"updated_at": "2021-05-11T21:13:25.566Z"}`, + want: dst{UpdatedAt: 1620767605}, + }, + { + name: "int", + json: `{"updated_at":1620767605}`, + want: dst{UpdatedAt: 1620767605}, + }, + { + name: "time parse error", + json: `{"updated_at":"foo"}`, + wantErr: true, + }, + { + name: "null", + json: `{"updated_at":null}`, + }, + { + name: "invalid type", + json: `{"updated_at":["foo","bar"]}`, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got dst + err := json.Unmarshal([]byte(tt.json), &got) + if tt.wantErr { + assert.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.want, got) + }) + } + t.Run("syntax error", func(t *testing.T) { + var ts Time + err := ts.UnmarshalJSON([]byte{'~'}) + assert.Error(t, err) + }) +} From 26d8e326361d1cdf60eaabe955d04e573578695a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 15 Mar 2023 15:32:14 +0200 Subject: [PATCH 194/502] chore: test all routes Co-authored-by: David Sharnoff --- example/server/storage/oidc.go | 8 +- example/server/storage/storage.go | 58 ++++- go.mod | 2 +- go.sum | 4 +- pkg/op/context.go | 22 +- pkg/op/device_test.go | 43 ---- pkg/op/op_test.go | 392 ++++++++++++++++++++++++++++++ 7 files changed, 467 insertions(+), 62 deletions(-) create mode 100644 pkg/op/op_test.go diff --git a/example/server/storage/oidc.go b/example/server/storage/oidc.go index 83db739..f5412cf 100644 --- a/example/server/storage/oidc.go +++ b/example/server/storage/oidc.go @@ -37,8 +37,8 @@ type AuthRequest struct { Nonce string CodeChallenge *OIDCCodeChallenge - passwordChecked bool - authTime time.Time + done bool + authTime time.Time } func (a *AuthRequest) GetID() string { @@ -51,7 +51,7 @@ func (a *AuthRequest) GetACR() string { func (a *AuthRequest) GetAMR() []string { // this example only uses password for authentication - if a.passwordChecked { + if a.done { return []string{"pwd"} } return nil @@ -102,7 +102,7 @@ func (a *AuthRequest) GetSubject() string { } func (a *AuthRequest) Done() bool { - return a.passwordChecked // this example only uses password for authentication + return a.done } func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string { diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index ff7889e..7e1afbd 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -28,8 +28,8 @@ var serviceKey1 = &rsa.PublicKey{ E: 65537, } -// var _ op.Storage = &storage{} -// var _ op.ClientCredentialsStorage = &storage{} +var _ op.Storage = &Storage{} +var _ op.ClientCredentialsStorage = &Storage{} // storage implements the op.Storage interface // typically you would implement this as a layer on top of your database @@ -46,6 +46,7 @@ type Storage struct { signingKey signingKey deviceCodes map[string]deviceAuthorizationEntry userCodes map[string]string + serviceUsers map[string]*Client } type signingKey struct { @@ -109,6 +110,16 @@ func NewStorage(userStore UserStore) *Storage { }, deviceCodes: make(map[string]deviceAuthorizationEntry), userCodes: make(map[string]string), + serviceUsers: map[string]*Client{ + "sid1": { + id: "sid1", + secret: "verysecret", + grantTypes: []oidc.GrantType{ + oidc.GrantTypeClientCredentials, + }, + accessTokenType: op.AccessTokenTypeBearer, + }, + }, } } @@ -133,7 +144,7 @@ func (s *Storage) CheckUsernamePassword(username, password, id string) error { // you will have to change some state on the request to guide the user through possible multiple steps of the login process // in this example we'll simply check the username / password and set a boolean to true // therefore we will also just check this boolean if the request / login has been finished - request.passwordChecked = true + request.done = true return nil } return fmt.Errorf("username or password wrong") @@ -847,3 +858,44 @@ func (s *Storage) DenyDeviceAuthorization(ctx context.Context, userCode string) s.deviceCodes[s.userCodes[userCode]].state.Denied = true return nil } + +// AuthRequestDone is used by testing and is not required to implement op.Storage +func (s *Storage) AuthRequestDone(id string) error { + s.lock.Lock() + defer s.lock.Unlock() + + if req, ok := s.authRequests[id]; ok { + req.done = true + return nil + } + + return errors.New("request not found") +} + +func (s *Storage) ClientCredentials(ctx context.Context, clientID, clientSecret string) (op.Client, error) { + s.lock.Lock() + defer s.lock.Unlock() + + client, ok := s.serviceUsers[clientID] + if !ok { + return nil, errors.New("wrong service user or password") + } + if client.secret != clientSecret { + return nil, errors.New("wrong service user or password") + } + + return client, nil +} + +func (s *Storage) ClientCredentialsTokenRequest(ctx context.Context, clientID string, scopes []string) (op.TokenRequest, error) { + client, ok := s.serviceUsers[clientID] + if !ok { + return nil, errors.New("wrong service user or password") + } + + return &oidc.JWTTokenRequest{ + Subject: client.id, + Audience: []string{clientID}, + Scopes: scopes, + }, nil +} diff --git a/go.mod b/go.mod index 9ed1e8e..50167f7 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/gorilla/schema v1.2.0 github.com/gorilla/securecookie v1.1.1 github.com/jeremija/gosubmit v0.2.7 - github.com/muhlemmer/gu v0.3.0 + github.com/muhlemmer/gu v0.3.1 github.com/rs/cors v1.8.3 github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.1 diff --git a/go.sum b/go.sum index 1933228..a5cf579 100644 --- a/go.sum +++ b/go.sum @@ -123,8 +123,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/muhlemmer/gu v0.3.0 h1:UwNv9xXGp1WDgHKgk7ljjh3duh1w4ZAY1k1NsWBYl3Y= -github.com/muhlemmer/gu v0.3.0/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= +github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= +github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/pkg/op/context.go b/pkg/op/context.go index 4406273..7cff5a7 100644 --- a/pkg/op/context.go +++ b/pkg/op/context.go @@ -7,16 +7,16 @@ import ( type key int -var ( - issuer key = 0 +const ( + issuerKey key = 0 ) type IssuerInterceptor struct { issuerFromRequest IssuerFromRequest } -//NewIssuerInterceptor will set the issuer into the context -//by the provided IssuerFromRequest (e.g. returned from StaticIssuer or IssuerFromHost) +// NewIssuerInterceptor will set the issuer into the context +// by the provided IssuerFromRequest (e.g. returned from StaticIssuer or IssuerFromHost) func NewIssuerInterceptor(issuerFromRequest IssuerFromRequest) *IssuerInterceptor { return &IssuerInterceptor{ issuerFromRequest: issuerFromRequest, @@ -35,15 +35,19 @@ func (i *IssuerInterceptor) HandlerFunc(next http.HandlerFunc) http.HandlerFunc } } -//IssuerFromContext reads the issuer from the context (set by an IssuerInterceptor) -//it will return an empty string if not found +// IssuerFromContext reads the issuer from the context (set by an IssuerInterceptor) +// it will return an empty string if not found func IssuerFromContext(ctx context.Context) string { - ctxIssuer, _ := ctx.Value(issuer).(string) + ctxIssuer, _ := ctx.Value(issuerKey).(string) return ctxIssuer } +// ContextWithIssuer returns a new context with issuer set to it. +func ContextWithIssuer(ctx context.Context, issuer string) context.Context { + return context.WithValue(ctx, issuerKey, issuer) +} + func (i *IssuerInterceptor) setIssuerCtx(w http.ResponseWriter, r *http.Request, next http.Handler) { - ctx := context.WithValue(r.Context(), issuer, i.issuerFromRequest(r)) - r = r.WithContext(ctx) + r = r.WithContext(ContextWithIssuer(r.Context(), i.issuerFromRequest(r))) next.ServeHTTP(w, r) } diff --git a/pkg/op/device_test.go b/pkg/op/device_test.go index de16a59..69ba102 100644 --- a/pkg/op/device_test.go +++ b/pkg/op/device_test.go @@ -3,7 +3,6 @@ package op_test import ( "context" "crypto/rand" - "crypto/sha256" "encoding/base64" "io" mr "math/rand" @@ -16,52 +15,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/example/server/storage" "github.com/zitadel/oidc/v2/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/op" - "golang.org/x/text/language" ) -var testProvider op.OpenIDProvider - -const ( - testIssuer = "https://localhost:9998/" - pathLoggedOut = "/logged-out" -) - -func init() { - config := &op.Config{ - CryptoKey: sha256.Sum256([]byte("test")), - DefaultLogoutRedirectURI: pathLoggedOut, - CodeMethodS256: true, - AuthMethodPost: true, - AuthMethodPrivateKeyJWT: true, - GrantTypeRefreshToken: true, - RequestObjectSupported: true, - SupportedUILocales: []language.Tag{language.English}, - DeviceAuthorization: op.DeviceAuthorizationConfig{ - Lifetime: 5 * time.Minute, - PollInterval: 5 * time.Second, - UserFormURL: testIssuer + "device", - UserCode: op.UserCodeBase20, - }, - } - - storage.RegisterClients( - storage.NativeClient("native"), - storage.WebClient("web", "secret"), - storage.WebClient("api", "secret"), - ) - - var err error - testProvider, err = op.NewOpenIDProvider(testIssuer, config, - storage.NewStorage(storage.NewUserStore(testIssuer)), op.WithAllowInsecure(), - ) - if err != nil { - panic(err) - } -} - func Test_deviceAuthorizationHandler(t *testing.T) { req := &oidc.DeviceAuthorizationRequest{ Scopes: []string{"foo", "bar"}, diff --git a/pkg/op/op_test.go b/pkg/op/op_test.go new file mode 100644 index 0000000..ba3570b --- /dev/null +++ b/pkg/op/op_test.go @@ -0,0 +1,392 @@ +package op_test + +import ( + "context" + "crypto/sha256" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/oidc/v2/example/server/storage" + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" + "golang.org/x/text/language" +) + +var testProvider op.OpenIDProvider + +const ( + testIssuer = "https://localhost:9998/" + pathLoggedOut = "/logged-out" +) + +func init() { + config := &op.Config{ + CryptoKey: sha256.Sum256([]byte("test")), + DefaultLogoutRedirectURI: pathLoggedOut, + CodeMethodS256: true, + AuthMethodPost: true, + AuthMethodPrivateKeyJWT: true, + GrantTypeRefreshToken: true, + RequestObjectSupported: true, + SupportedUILocales: []language.Tag{language.English}, + DeviceAuthorization: op.DeviceAuthorizationConfig{ + Lifetime: 5 * time.Minute, + PollInterval: 5 * time.Second, + UserFormURL: testIssuer + "device", + UserCode: op.UserCodeBase20, + }, + } + + storage.RegisterClients( + storage.NativeClient("native"), + storage.WebClient("web", "secret", "https://example.com"), + storage.WebClient("api", "secret"), + ) + + var err error + testProvider, err = op.NewOpenIDProvider(testIssuer, config, + storage.NewStorage(storage.NewUserStore(testIssuer)), op.WithAllowInsecure(), + ) + if err != nil { + panic(err) + } +} + +type routesTestStorage interface { + op.Storage + AuthRequestDone(id string) error +} + +func mapAsValues(m map[string]string) string { + values := make(url.Values, len(m)) + for k, v := range m { + values.Set(k, v) + } + return values.Encode() +} + +func TestRoutes(t *testing.T) { + storage := testProvider.Storage().(routesTestStorage) + ctx := op.ContextWithIssuer(context.Background(), testIssuer) + + client, err := storage.GetClientByClientID(ctx, "web") + require.NoError(t, err) + + oidcAuthReq := &oidc.AuthRequest{ + ClientID: client.GetID(), + RedirectURI: "https://example.com", + MaxAge: gu.Ptr[uint](300), + Scopes: oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess, oidc.ScopeEmail, oidc.ScopeProfile, oidc.ScopePhone}, + ResponseType: oidc.ResponseTypeCode, + } + + authReq, err := storage.CreateAuthRequest(ctx, oidcAuthReq, "id1") + require.NoError(t, err) + storage.AuthRequestDone(authReq.GetID()) + + accessToken, refreshToken, _, err := op.CreateAccessToken(ctx, authReq, op.AccessTokenTypeBearer, testProvider, client, "") + require.NoError(t, err) + accessTokenRevoke, _, _, err := op.CreateAccessToken(ctx, authReq, op.AccessTokenTypeBearer, testProvider, client, "") + require.NoError(t, err) + idToken, err := op.CreateIDToken(ctx, testIssuer, authReq, time.Hour, accessToken, "123", storage, client) + require.NoError(t, err) + jwtToken, _, _, err := op.CreateAccessToken(ctx, authReq, op.AccessTokenTypeJWT, testProvider, client, "") + require.NoError(t, err) + + oidcAuthReq.IDTokenHint = idToken + + serverURL, err := url.Parse(testIssuer) + require.NoError(t, err) + + type basicAuth struct { + username, password string + } + + tests := []struct { + name string + method string + path string + basicAuth *basicAuth + header map[string]string + values map[string]string + body map[string]string + wantCode int + headerContains map[string]string + json string // test for exact json output + contains []string // when the body output is not constant, we just check for snippets to be present in the response + }{ + { + name: "health", + method: http.MethodGet, + path: "/healthz", + wantCode: http.StatusOK, + json: `{"status":"ok"}`, + }, + { + name: "ready", + method: http.MethodGet, + path: "/ready", + wantCode: http.StatusOK, + json: `{"status":"ok"}`, + }, + { + name: "discovery", + method: http.MethodGet, + path: oidc.DiscoveryEndpoint, + wantCode: http.StatusOK, + json: `{"issuer":"https://localhost:9998/","authorization_endpoint":"https://localhost:9998/authorize","token_endpoint":"https://localhost:9998/oauth/token","introspection_endpoint":"https://localhost:9998/oauth/introspect","userinfo_endpoint":"https://localhost:9998/userinfo","revocation_endpoint":"https://localhost:9998/revoke","end_session_endpoint":"https://localhost:9998/end_session","device_authorization_endpoint":"https://localhost:9998/device_authorization","jwks_uri":"https://localhost:9998/keys","scopes_supported":["openid","profile","email","phone","address","offline_access"],"response_types_supported":["code","id_token","id_token token"],"grant_types_supported":["authorization_code","implicit","refresh_token","client_credentials","urn:ietf:params:oauth:grant-type:token-exchange","urn:ietf:params:oauth:grant-type:jwt-bearer","urn:ietf:params:oauth:grant-type:device_code"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"],"request_object_signing_alg_values_supported":["RS256"],"token_endpoint_auth_methods_supported":["none","client_secret_basic","client_secret_post","private_key_jwt"],"token_endpoint_auth_signing_alg_values_supported":["RS256"],"revocation_endpoint_auth_methods_supported":["none","client_secret_basic","client_secret_post","private_key_jwt"],"revocation_endpoint_auth_signing_alg_values_supported":["RS256"],"introspection_endpoint_auth_methods_supported":["client_secret_basic","private_key_jwt"],"introspection_endpoint_auth_signing_alg_values_supported":["RS256"],"claims_supported":["sub","aud","exp","iat","iss","auth_time","nonce","acr","amr","c_hash","at_hash","act","scopes","client_id","azp","preferred_username","name","family_name","given_name","locale","email","email_verified","phone_number","phone_number_verified"],"code_challenge_methods_supported":["S256"],"ui_locales_supported":["en"],"request_parameter_supported":true,"request_uri_parameter_supported":false}`, + }, + { + name: "authorization", + method: http.MethodGet, + path: testProvider.AuthorizationEndpoint().Relative(), + values: map[string]string{ + "client_id": client.GetID(), + "redirect_uri": "https://example.com", + "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(), + "response_type": string(oidc.ResponseTypeCode), + }, + wantCode: http.StatusFound, + headerContains: map[string]string{"Location": "/login/username?authRequestID="}, + }, + { + name: "authorization callback", + method: http.MethodGet, + path: testProvider.AuthorizationEndpoint().Relative() + "/callback", + values: map[string]string{"id": authReq.GetID()}, + wantCode: http.StatusFound, + headerContains: map[string]string{"Location": "https://example.com?code="}, + contains: []string{ + `Found.", + }, + }, + { + // This call will fail. A successfull test is already + // part of client/integration_test.go + name: "code exchange", + method: http.MethodGet, + path: testProvider.TokenEndpoint().Relative(), + values: map[string]string{ + "grant_type": string(oidc.GrantTypeCode), + "code": "123", + }, + wantCode: http.StatusUnauthorized, + json: `{"error":"invalid_client"}`, + }, + { + name: "JWT authorization", + method: http.MethodGet, + path: testProvider.TokenEndpoint().Relative(), + values: map[string]string{ + "grant_type": string(oidc.GrantTypeBearer), + "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(), + "assertion": jwtToken, + }, + wantCode: http.StatusBadRequest, + json: "{\"error\":\"server_error\",\"error_description\":\"audience is not valid: Audience must contain client_id \\\"https://localhost:9998/\\\"\"}", + }, + { + name: "Token exchange", + method: http.MethodGet, + path: testProvider.TokenEndpoint().Relative(), + basicAuth: &basicAuth{"web", "secret"}, + values: map[string]string{ + "grant_type": string(oidc.GrantTypeTokenExchange), + "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(), + "subject_token": jwtToken, + "subject_token_type": string(oidc.AccessTokenType), + }, + wantCode: http.StatusOK, + contains: []string{ + `{"access_token":"`, + `","issued_token_type":"urn:ietf:params:oauth:token-type:refresh_token","token_type":"Bearer","expires_in":299,"scope":"openid offline_access","refresh_token":"`, + }, + }, + { + name: "Client credentials exchange", + method: http.MethodGet, + path: testProvider.TokenEndpoint().Relative(), + basicAuth: &basicAuth{"sid1", "verysecret"}, + values: map[string]string{ + "grant_type": string(oidc.GrantTypeClientCredentials), + "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(), + }, + wantCode: http.StatusOK, + contains: []string{`{"access_token":"`, `","token_type":"Bearer","expires_in":299}`}, + }, + { + // This call will fail. A successfull test is already + // part of device_test.go + name: "device token", + method: http.MethodPost, + path: testProvider.TokenEndpoint().Relative(), + basicAuth: &basicAuth{"web", "secret"}, + header: map[string]string{ + "Content-Type": "application/x-www-form-urlencoded", + }, + body: map[string]string{ + "grant_type": string(oidc.GrantTypeDeviceCode), + "device_code": "123", + }, + wantCode: http.StatusBadRequest, + json: `{"error":"access_denied","error_description":"The authorization request was denied."}`, + }, + { + name: "missing grant type", + method: http.MethodGet, + path: testProvider.TokenEndpoint().Relative(), + wantCode: http.StatusBadRequest, + json: `{"error":"invalid_request","error_description":"grant_type missing"}`, + }, + { + name: "unsupported grant type", + method: http.MethodGet, + path: testProvider.TokenEndpoint().Relative(), + values: map[string]string{ + "grant_type": "foo", + }, + wantCode: http.StatusBadRequest, + json: `{"error":"unsupported_grant_type","error_description":"foo not supported"}`, + }, + { + name: "introspection", + method: http.MethodGet, + path: testProvider.IntrospectionEndpoint().Relative(), + basicAuth: &basicAuth{"web", "secret"}, + values: map[string]string{ + "token": accessToken, + }, + wantCode: http.StatusOK, + json: `{"active":true,"scope":"openid offline_access email profile phone","client_id":"web","sub":"id1","username":"test-user@localhost","name":"Test User","given_name":"Test","family_name":"User","locale":"de","preferred_username":"test-user@localhost","email":"test-user@zitadel.ch","email_verified":true}`, + }, + { + name: "user info", + method: http.MethodGet, + path: testProvider.UserinfoEndpoint().Relative(), + header: map[string]string{ + "authorization": "Bearer " + accessToken, + }, + wantCode: http.StatusOK, + json: `{"sub":"id1","name":"Test User","given_name":"Test","family_name":"User","locale":"de","preferred_username":"test-user@localhost","email":"test-user@zitadel.ch","email_verified":true}`, + }, + { + name: "refresh token", + method: http.MethodGet, + path: testProvider.TokenEndpoint().Relative(), + values: map[string]string{ + "grant_type": string(oidc.GrantTypeRefreshToken), + "refresh_token": refreshToken, + "client_id": client.GetID(), + "client_secret": "secret", + }, + wantCode: http.StatusOK, + contains: []string{ + `{"access_token":"`, + `","token_type":"Bearer","refresh_token":"`, + `","expires_in":299,"id_token":"`, + }, + }, + { + name: "revoke", + method: http.MethodGet, + path: testProvider.RevocationEndpoint().Relative(), + basicAuth: &basicAuth{"web", "secret"}, + values: map[string]string{ + "token": accessTokenRevoke, + }, + wantCode: http.StatusOK, + }, + { + name: "end session", + method: http.MethodGet, + path: testProvider.EndSessionEndpoint().Relative(), + values: map[string]string{ + "id_token_hint": idToken, + "client_id": "web", + }, + wantCode: http.StatusFound, + headerContains: map[string]string{"Location": "/logged-out"}, + contains: []string{`Found.`}, + }, + { + name: "keys", + method: http.MethodGet, + path: testProvider.KeysEndpoint().Relative(), + wantCode: http.StatusOK, + contains: []string{ + `{"keys":[{"use":"sig","kty":"RSA","kid":"`, + `","alg":"RS256","n":"`, `","e":"AQAB"}]}`, + }, + }, + { + name: "device authorization", + method: http.MethodGet, + path: testProvider.DeviceAuthorizationEndpoint().Relative(), + basicAuth: &basicAuth{"web", "secret"}, + values: map[string]string{ + "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(), + }, + wantCode: http.StatusOK, + contains: []string{ + `{"device_code":"`, `","user_code":"`, + `","verification_uri":"https://localhost:9998/device"`, + `"verification_uri_complete":"https://localhost:9998/device?user_code=`, + `","expires_in":300,"interval":5}`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u := gu.PtrCopy(serverURL) + u.Path = tt.path + if tt.values != nil { + u.RawQuery = mapAsValues(tt.values) + } + var body io.Reader + if tt.body != nil { + body = strings.NewReader(mapAsValues(tt.body)) + } + + req := httptest.NewRequest(tt.method, u.String(), body) + for k, v := range tt.header { + req.Header.Set(k, v) + } + if tt.basicAuth != nil { + req.SetBasicAuth(tt.basicAuth.username, tt.basicAuth.password) + } + + rec := httptest.NewRecorder() + testProvider.HttpHandler().ServeHTTP(rec, req) + + resp := rec.Result() + require.NoError(t, err) + assert.Equal(t, tt.wantCode, resp.StatusCode) + + respBody, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + respBodyString := string(respBody) + t.Log(respBodyString) + t.Log(resp.Header) + + if tt.json != "" { + assert.JSONEq(t, tt.json, respBodyString) + } + for _, c := range tt.contains { + assert.Contains(t, respBodyString, c) + } + for k, v := range tt.headerContains { + assert.Contains(t, resp.Header.Get(k), v) + } + }) + } +} From 0f3d4f4828e028d6a4a915990af556bbccda51c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 15 Mar 2023 15:37:02 +0200 Subject: [PATCH 195/502] chore: update all modules (#321) --- go.mod | 25 ++-- go.sum | 375 +++++---------------------------------------------------- 2 files changed, 42 insertions(+), 358 deletions(-) diff --git a/go.mod b/go.mod index 50167f7..7594264 100644 --- a/go.mod +++ b/go.mod @@ -13,24 +13,23 @@ require ( github.com/muhlemmer/gu v0.3.1 github.com/rs/cors v1.8.3 github.com/sirupsen/logrus v1.9.0 - github.com/stretchr/testify v1.8.1 - golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 - golang.org/x/text v0.6.0 + github.com/stretchr/testify v1.8.2 + golang.org/x/oauth2 v0.6.0 + golang.org/x/text v0.8.0 gopkg.in/square/go-jose.v2 v2.6.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/golang/protobuf v1.4.2 // indirect - github.com/google/go-cmp v0.5.2 // indirect - github.com/google/go-querystring v1.0.0 // indirect - github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect - golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect - google.golang.org/appengine v1.6.6 // indirect - google.golang.org/protobuf v1.25.0 // indirect - gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect + golang.org/x/crypto v0.7.0 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/sys v0.6.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.29.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a5cf579..61083fb 100644 --- a/go.sum +++ b/go.sum @@ -1,125 +1,34 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -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= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -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= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo= github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= -github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc= github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -129,8 +38,6 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo= github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= @@ -138,285 +45,63 @@ github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -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/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -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= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -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/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 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= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -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/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= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0= +google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= From c6820ba88a24df28bd2c6e542183f845b22a900f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 15 Mar 2023 15:44:49 +0200 Subject: [PATCH 196/502] fix: unmarshalling of scopes in access token (#327) The Scopes field in accessTokenClaims should be a SpaceDelimitedArray, in order to allow for correct unmarshalling. Fixes #318 * adjust test data --- pkg/oidc/regression_data/oidc.AccessTokenClaims.json | 5 +---- pkg/oidc/token.go | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/pkg/oidc/regression_data/oidc.AccessTokenClaims.json b/pkg/oidc/regression_data/oidc.AccessTokenClaims.json index e4f7808..b63bf30 100644 --- a/pkg/oidc/regression_data/oidc.AccessTokenClaims.json +++ b/pkg/oidc/regression_data/oidc.AccessTokenClaims.json @@ -13,10 +13,7 @@ "some", "methods" ], - "scope": [ - "email", - "phone" - ], + "scope": "email phone", "client_id": "777", "exp": 12345, "iat": 12000, diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index 1ade913..b017023 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -97,8 +97,8 @@ func (c *TokenClaims) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) { type AccessTokenClaims struct { TokenClaims - Scopes []string `json:"scope,omitempty"` - Claims map[string]any `json:"-"` + Scopes SpaceDelimitedArray `json:"scope,omitempty"` + Claims map[string]any `json:"-"` } func NewAccessTokenClaims(issuer, subject string, audience []string, expiration time.Time, jwtid, clientID string, skew time.Duration) *AccessTokenClaims { From 62caf5dafeaaf044410f9fa81653682888ddc03a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Thu, 16 Mar 2023 18:08:56 +0200 Subject: [PATCH 197/502] chore: update features in readme - rotated features table for better rendering - add links to specifications in feature table - remove redundant links from the resources section - changed "Token Exhange" feature to full yes (PR #255) - add "Device Authorization" with full yes (PR #285) --- README.md | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c47e192..f369a5c 100644 --- a/README.md +++ b/README.md @@ -67,10 +67,31 @@ CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid ## Features -| | Code Flow | Implicit Flow | Hybrid Flow | Discovery | PKCE | Token Exchange | mTLS | JWT Profile | Refresh Token | Client Credentials | -|------------------|-----------|---------------|-------------|-----------|------|----------------|---------|-------------|---------------|--------------------| -| Relying Party | yes | no[^1] | no | yes | yes | partial | not yet | yes | yes | not yet | -| OpenID Provider | yes | yes | not yet | yes | yes | not yet | not yet | yes | yes | yes | +| | Relying party | OpenID Provider | Specification | +| -------------------- | ------------- | --------------- | ----------------------------------------- | +| Code Flow | yes | yes | OpenID Connect Core 1.0, [Section 3.1][1] | +| Implicit Flow | no[^1] | yes | OpenID Connect Core 1.0, [Section 3.2][2] | +| Hybrid Flow | no | not yet | OpenID Connect Core 1.0, [Section 3.3][3] | +| Client Credentials | not yet | yes | OpenID Connect Core 1.0, [Section 9][4] | +| Refresh Token | yes | yes | OpenID Connect Core 1.0, [Section 12][5] | +| Discovery | yes | yes | OpenID Connect [Discovery][6] 1.0 | +| JWT Profile | yes | yes | [RFC 7523][7] | +| PKCE | yes | yes | [RFC 7636][8] | +| Token Exchange | yes | yes | [RFC 8693][9] | +| Device Authorization | yes | yes | [RFC 8628][10] | +| mTLS | not yet | not yet | [RFC 8705][11] | + +[1]: "3.1. Authentication using the Authorization Code Flow" +[2]: "3.2. Authentication using the Implicit Flow" +[3]: "3.3. Authentication using the Hybrid Flow" +[4]: "9. Client Authentication" +[5]: "12. Using Refresh Tokens" +[6]: "OpenID Connect Discovery 1.0 incorporating errata set 1" +[7]: "JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants" +[8]: "Proof Key for Code Exchange by OAuth Public Clients" +[9]: "OAuth 2.0 Token Exchange" +[10]: "OAuth 2.0 Device Authorization Grant" +[11]: "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens" ## Contributors @@ -82,13 +103,9 @@ Made with [contrib.rocks](https://contrib.rocks). ### Resources -For your convenience you can find the relevant standards linked below. +For your convenience you can find the relevant guides linked below. - [OpenID Connect Core 1.0 incorporating errata set 1](https://openid.net/specs/openid-connect-core-1_0.html) -- [Proof Key for Code Exchange by OAuth Public Clients](https://tools.ietf.org/html/rfc7636) -- [OAuth 2.0 Token Exchange](https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-19) -- [OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens](https://tools.ietf.org/html/draft-ietf-oauth-mtls-17) -- [JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants](https://tools.ietf.org/html/rfc7523) - [OIDC/OAuth Flow in Zitadel (using this library)](https://zitadel.com/docs/guides/integrate/login-users) ## Supported Go Versions @@ -97,7 +114,7 @@ For security reasons, we only support and recommend the use of one of the latest Versions that also build are marked with :warning:. | Version | Supported | -|---------|--------------------| +| ------- | ------------------ | | <1.18 | :x: | | 1.18 | :warning: | | 1.19 | :white_check_mark: | From bb392314d835b5dacb705cc0a3e6e8f20e92fc13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Mar 2023 13:02:07 +0000 Subject: [PATCH 198/502] chore(deps): bump google.golang.org/protobuf from 1.29.0 to 1.29.1 Bumps [google.golang.org/protobuf](https://github.com/protocolbuffers/protobuf-go) from 1.29.0 to 1.29.1. - [Release notes](https://github.com/protocolbuffers/protobuf-go/releases) - [Changelog](https://github.com/protocolbuffers/protobuf-go/blob/master/release.bash) - [Commits](https://github.com/protocolbuffers/protobuf-go/compare/v1.29.0...v1.29.1) --- updated-dependencies: - dependency-name: google.golang.org/protobuf dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 15 ++------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 7594264..adb638e 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( golang.org/x/net v0.8.0 // indirect golang.org/x/sys v0.6.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.29.0 // indirect + google.golang.org/protobuf v1.29.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e4e5c6c..4259674 100644 --- a/go.sum +++ b/go.sum @@ -34,8 +34,6 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo= @@ -50,9 +48,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -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/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -84,12 +79,6 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -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/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -104,8 +93,8 @@ google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0= -google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM= +google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 57fb9f77aa5db4486af67a67132063a3b841abae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 17 Mar 2023 17:36:02 +0200 Subject: [PATCH 199/502] chore: replace gorilla/mux with go-chi/chi (#332) BREAKING CHANGE: The returned router from `op.CreateRouter()` is now a `chi.Router` Closes #301 --- example/client/api/api.go | 10 ++++---- example/server/dynamic/login.go | 10 ++++---- example/server/dynamic/op.go | 8 +++---- example/server/exampleop/device.go | 8 +++---- example/server/exampleop/login.go | 10 ++++---- example/server/exampleop/op.go | 15 ++++++------ go.mod | 2 +- go.sum | 15 ++---------- pkg/op/auth_request.go | 22 +++++++++++------ pkg/op/auth_request_test.go | 38 +++++++++++++++++++++++++++++- pkg/op/op.go | 18 ++++++++------ pkg/op/op_test.go | 2 +- 12 files changed, 98 insertions(+), 60 deletions(-) diff --git a/example/client/api/api.go b/example/client/api/api.go index 8093b63..9f654a9 100644 --- a/example/client/api/api.go +++ b/example/client/api/api.go @@ -9,7 +9,7 @@ import ( "strings" "time" - "github.com/gorilla/mux" + "github.com/go-chi/chi" "github.com/sirupsen/logrus" "github.com/zitadel/oidc/v2/pkg/client/rs" @@ -32,7 +32,7 @@ func main() { logrus.Fatalf("error creating provider %s", err.Error()) } - router := mux.NewRouter() + router := chi.NewRouter() // public url accessible without any authorization // will print `OK` and current timestamp @@ -73,9 +73,9 @@ func main() { http.Error(w, err.Error(), http.StatusForbidden) return } - params := mux.Vars(r) - requestedClaim := params["claim"] - requestedValue := params["value"] + requestedClaim := chi.URLParam(r, "claim") + requestedValue := chi.URLParam(r, "value") + value, ok := resp.Claims[requestedClaim].(string) if !ok || value == "" || value != requestedValue { http.Error(w, "claim does not match", http.StatusForbidden) diff --git a/example/server/dynamic/login.go b/example/server/dynamic/login.go index e7c6e5f..eb5340e 100644 --- a/example/server/dynamic/login.go +++ b/example/server/dynamic/login.go @@ -6,7 +6,7 @@ import ( "html/template" "net/http" - "github.com/gorilla/mux" + "github.com/go-chi/chi" "github.com/zitadel/oidc/v2/pkg/op" ) @@ -43,7 +43,7 @@ var ( type login struct { authenticate authenticate - router *mux.Router + router chi.Router callback func(context.Context, string) string } @@ -57,9 +57,9 @@ func NewLogin(authenticate authenticate, callback func(context.Context, string) } func (l *login) createRouter(issuerInterceptor *op.IssuerInterceptor) { - l.router = mux.NewRouter() - l.router.Path("/username").Methods("GET").HandlerFunc(l.loginHandler) - l.router.Path("/username").Methods("POST").HandlerFunc(issuerInterceptor.HandlerFunc(l.checkLoginHandler)) + l.router = chi.NewRouter() + l.router.Get("/username", l.loginHandler) + l.router.With(issuerInterceptor.Handler).Post("/username", l.checkLoginHandler) } type authenticate interface { diff --git a/example/server/dynamic/op.go b/example/server/dynamic/op.go index 783c75c..2bb6832 100644 --- a/example/server/dynamic/op.go +++ b/example/server/dynamic/op.go @@ -7,7 +7,7 @@ import ( "log" "net/http" - "github.com/gorilla/mux" + "github.com/go-chi/chi" "golang.org/x/text/language" "github.com/zitadel/oidc/v2/example/server/storage" @@ -47,7 +47,7 @@ func main() { //be sure to create a proper crypto random key and manage it securely! key := sha256.Sum256([]byte("test")) - router := mux.NewRouter() + router := chi.NewRouter() //for simplicity, we provide a very small default page for users who have signed out router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) { @@ -76,7 +76,7 @@ func main() { //regardless of how many pages / steps there are in the process, the UI must be registered in the router, //so we will direct all calls to /login to the login UI - router.PathPrefix("/login/").Handler(http.StripPrefix("/login", l.router)) + router.Mount("/login/", http.StripPrefix("/login", l.router)) //we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration) //is served on the correct path @@ -84,7 +84,7 @@ func main() { //if your issuer ends with a path (e.g. http://localhost:9998/custom/path/), //then you would have to set the path prefix (/custom/path/): //router.PathPrefix("/custom/path/").Handler(http.StripPrefix("/custom/path", provider.HttpHandler())) - router.PathPrefix("/").Handler(provider.HttpHandler()) + router.Mount("/", provider) server := &http.Server{ Addr: ":" + port, diff --git a/example/server/exampleop/device.go b/example/server/exampleop/device.go index ae2e8f2..59c2196 100644 --- a/example/server/exampleop/device.go +++ b/example/server/exampleop/device.go @@ -7,7 +7,7 @@ import ( "net/http" "net/url" - "github.com/gorilla/mux" + "github.com/go-chi/chi" "github.com/gorilla/securecookie" "github.com/sirupsen/logrus" "github.com/zitadel/oidc/v2/pkg/op" @@ -23,14 +23,14 @@ type deviceLogin struct { cookie *securecookie.SecureCookie } -func registerDeviceAuth(storage deviceAuthenticate, router *mux.Router) { +func registerDeviceAuth(storage deviceAuthenticate, router chi.Router) { l := &deviceLogin{ storage: storage, cookie: securecookie.New(securecookie.GenerateRandomKey(32), nil), } - router.HandleFunc("", l.userCodeHandler) - router.Path("/login").Methods(http.MethodPost).HandlerFunc(l.loginHandler) + router.HandleFunc("/", l.userCodeHandler) + router.Post("/login", l.loginHandler) router.HandleFunc("/confirm", l.confirmHandler) } diff --git a/example/server/exampleop/login.go b/example/server/exampleop/login.go index c014c9a..9facb90 100644 --- a/example/server/exampleop/login.go +++ b/example/server/exampleop/login.go @@ -5,12 +5,12 @@ import ( "fmt" "net/http" - "github.com/gorilla/mux" + "github.com/go-chi/chi" ) type login struct { authenticate authenticate - router *mux.Router + router chi.Router callback func(context.Context, string) string } @@ -24,9 +24,9 @@ func NewLogin(authenticate authenticate, callback func(context.Context, string) } func (l *login) createRouter() { - l.router = mux.NewRouter() - l.router.Path("/username").Methods("GET").HandlerFunc(l.loginHandler) - l.router.Path("/username").Methods("POST").HandlerFunc(l.checkLoginHandler) + l.router = chi.NewRouter() + l.router.Get("/username", l.loginHandler) + l.router.Post("/username", l.checkLoginHandler) } type authenticate interface { diff --git a/example/server/exampleop/op.go b/example/server/exampleop/op.go index 5604483..077244c 100644 --- a/example/server/exampleop/op.go +++ b/example/server/exampleop/op.go @@ -6,7 +6,7 @@ import ( "net/http" "time" - "github.com/gorilla/mux" + "github.com/go-chi/chi" "golang.org/x/text/language" "github.com/zitadel/oidc/v2/example/server/storage" @@ -34,12 +34,12 @@ type Storage interface { // SetupServer creates an OIDC server with Issuer=http://localhost: // // Use one of the pre-made clients in storage/clients.go or register a new one. -func SetupServer(issuer string, storage Storage) *mux.Router { +func SetupServer(issuer string, storage Storage) chi.Router { // the OpenID Provider requires a 32-byte key for (token) encryption // be sure to create a proper crypto random key and manage it securely! key := sha256.Sum256([]byte("test")) - router := mux.NewRouter() + router := chi.NewRouter() // for simplicity, we provide a very small default page for users who have signed out router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) { @@ -61,17 +61,18 @@ func SetupServer(issuer string, storage Storage) *mux.Router { // regardless of how many pages / steps there are in the process, the UI must be registered in the router, // so we will direct all calls to /login to the login UI - router.PathPrefix("/login/").Handler(http.StripPrefix("/login", l.router)) + router.Mount("/login/", http.StripPrefix("/login", l.router)) - router.PathPrefix("/device").Subrouter() - registerDeviceAuth(storage, router.PathPrefix("/device").Subrouter()) + router.Route("/device", func(r chi.Router) { + registerDeviceAuth(storage, r) + }) // we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration) // is served on the correct path // // if your issuer ends with a path (e.g. http://localhost:9998/custom/path/), // then you would have to set the path prefix (/custom/path/) - router.PathPrefix("/").Handler(provider.HttpHandler()) + router.Mount("/", provider) return router } diff --git a/go.mod b/go.mod index 7594264..a636250 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module github.com/zitadel/oidc/v2 go 1.18 require ( + github.com/go-chi/chi v1.5.4 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 github.com/google/uuid v1.3.0 - github.com/gorilla/mux v1.8.0 github.com/gorilla/schema v1.2.0 github.com/gorilla/securecookie v1.1.1 github.com/jeremija/gosubmit v0.2.7 diff --git a/go.sum b/go.sum index e4e5c6c..a5ba642 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ 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/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= +github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -19,8 +21,6 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= @@ -34,8 +34,6 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo= @@ -50,9 +48,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -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/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -84,12 +79,6 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -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/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index b312098..4c48363 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -2,6 +2,7 @@ package op import ( "context" + "errors" "fmt" "net" "net/http" @@ -10,8 +11,6 @@ import ( "strings" "time" - "github.com/gorilla/mux" - httphelper "github.com/zitadel/oidc/v2/pkg/http" "github.com/zitadel/oidc/v2/pkg/oidc" str "github.com/zitadel/oidc/v2/pkg/strings" @@ -405,13 +404,11 @@ func RedirectToLogin(authReqID string, client Client, w http.ResponseWriter, r * // AuthorizeCallback handles the callback after authentication in the Login UI func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { - params := mux.Vars(r) - id := params["id"] - if id == "" { - AuthRequestError(w, r, nil, fmt.Errorf("auth request callback is missing id"), authorizer.Encoder()) + id, err := ParseAuthorizeCallbackRequest(r) + if err != nil { + AuthRequestError(w, r, nil, err, authorizer.Encoder()) return } - authReq, err := authorizer.Storage().AuthRequestByID(r.Context(), id) if err != nil { AuthRequestError(w, r, nil, err, authorizer.Encoder()) @@ -426,6 +423,17 @@ func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Author AuthResponse(authReq, authorizer, w, r) } +func ParseAuthorizeCallbackRequest(r *http.Request) (id string, err error) { + if err = r.ParseForm(); err != nil { + return "", fmt.Errorf("cannot parse form: %w", err) + } + id = r.Form.Get("id") + if id == "" { + return "", errors.New("auth request callback is missing id") + } + return id, nil +} + // AuthResponse creates the successful authentication response (either code or tokens) func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWriter, r *http.Request) { client, err := authorizer.Storage().GetClientByClientID(r.Context(), authReq.GetClientID()) diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index 7a9701b..542f2e2 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -12,7 +12,6 @@ import ( "github.com/gorilla/schema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - httphelper "github.com/zitadel/oidc/v2/pkg/http" "github.com/zitadel/oidc/v2/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/op" @@ -967,3 +966,40 @@ func (m *mockEncoder) Encode(src interface{}, dst map[string][]string) error { } return nil } + +func Test_parseAuthorizeCallbackRequest(t *testing.T) { + tests := []struct { + name string + url string + wantId string + wantErr bool + }{ + { + name: "parse error", + url: "/?id;=99", + wantErr: true, + }, + { + name: "missing id", + url: "/", + wantErr: true, + }, + { + name: "ok", + url: "/?id=99", + wantId: "99", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, tt.url, nil) + gotId, err := op.ParseAuthorizeCallbackRequest(r) + if tt.wantErr { + assert.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.wantId, gotId) + }) + } +} diff --git a/pkg/op/op.go b/pkg/op/op.go index ecb753e..0536bbc 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -6,7 +6,7 @@ import ( "net/http" "time" - "github.com/gorilla/mux" + "github.com/go-chi/chi" "github.com/gorilla/schema" "github.com/rs/cors" "golang.org/x/text/language" @@ -68,6 +68,7 @@ var ( ) type OpenIDProvider interface { + http.Handler Configuration Storage() Storage Decoder() httphelper.Decoder @@ -77,20 +78,22 @@ type OpenIDProvider interface { Crypto() Crypto DefaultLogoutRedirectURI() string Probes() []ProbesFn + + // Deprecated: Provider now implements http.Handler directly. HttpHandler() http.Handler } type HttpInterceptor func(http.Handler) http.Handler -func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router { - router := mux.NewRouter() +func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) chi.Router { + router := chi.NewRouter() router.Use(cors.New(defaultCORSOptions).Handler) router.Use(intercept(o.IssuerFromRequest, interceptors...)) router.HandleFunc(healthEndpoint, healthHandler) router.HandleFunc(readinessEndpoint, readyHandler(o.Probes())) router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Storage())) router.HandleFunc(o.AuthorizationEndpoint().Relative(), authorizeHandler(o)) - router.NewRoute().Path(authCallbackPath(o)).Queries("id", "{id}").HandlerFunc(authorizeCallbackHandler(o)) + router.HandleFunc(authCallbackPath(o), authorizeCallbackHandler(o)) router.HandleFunc(o.TokenEndpoint().Relative(), tokenHandler(o)) router.HandleFunc(o.IntrospectionEndpoint().Relative(), introspectionHandler(o)) router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o)) @@ -184,7 +187,7 @@ func newProvider(config *Config, storage Storage, issuer func(bool) (IssuerFromR return nil, err } - o.httpHandler = CreateRouter(o, o.interceptors...) + o.Handler = CreateRouter(o, o.interceptors...) o.decoder = schema.NewDecoder() o.decoder.IgnoreUnknownKeys(true) @@ -200,6 +203,7 @@ func newProvider(config *Config, storage Storage, issuer func(bool) (IssuerFromR } type Provider struct { + http.Handler config *Config issuer IssuerFromRequest insecure bool @@ -207,7 +211,6 @@ type Provider struct { storage Storage keySet *openIDKeySet crypto Crypto - httpHandler http.Handler decoder *schema.Decoder encoder *schema.Encoder interceptors []HttpInterceptor @@ -372,8 +375,9 @@ func (o *Provider) Probes() []ProbesFn { } } +// Deprecated: Provider now implements http.Handler directly. func (o *Provider) HttpHandler() http.Handler { - return o.httpHandler + return o } type openIDKeySet struct { diff --git a/pkg/op/op_test.go b/pkg/op/op_test.go index ba3570b..8429212 100644 --- a/pkg/op/op_test.go +++ b/pkg/op/op_test.go @@ -365,7 +365,7 @@ func TestRoutes(t *testing.T) { } rec := httptest.NewRecorder() - testProvider.HttpHandler().ServeHTTP(rec, req) + testProvider.ServeHTTP(rec, req) resp := rec.Result() require.NoError(t, err) From 890a7f3ed4cf4e258159dbe500cd8be82c56afba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Mon, 20 Mar 2023 11:06:32 +0200 Subject: [PATCH 200/502] feat: GetUserinfo helper method for IDTokenClaims (#337) --- pkg/oidc/token.go | 11 +++++++++++ pkg/oidc/token_test.go | 13 +++++++++++++ 2 files changed, 24 insertions(+) diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index b017023..127db97 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -159,6 +159,17 @@ func (t *IDTokenClaims) SetUserInfo(i *UserInfo) { t.Address = i.Address } +func (t *IDTokenClaims) GetUserInfo() *UserInfo { + return &UserInfo{ + Subject: t.Subject, + UserInfoProfile: t.UserInfoProfile, + UserInfoEmail: t.UserInfoEmail, + UserInfoPhone: t.UserInfoPhone, + Address: t.Address, + Claims: t.Claims, + } +} + func NewIDTokenClaims(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string, skew time.Duration) *IDTokenClaims { audience = AppendClientIDToAudience(clientID, audience) return &IDTokenClaims{ diff --git a/pkg/oidc/token_test.go b/pkg/oidc/token_test.go index 0d9874e..8dcfc7e 100644 --- a/pkg/oidc/token_test.go +++ b/pkg/oidc/token_test.go @@ -225,3 +225,16 @@ func TestNewIDTokenClaims(t *testing.T) { assert.Equal(t, want, got) } + +func TestIDTokenClaims_GetUserInfo(t *testing.T) { + want := &UserInfo{ + Subject: idTokenData.Subject, + UserInfoProfile: idTokenData.UserInfoProfile, + UserInfoEmail: idTokenData.UserInfoEmail, + UserInfoPhone: idTokenData.UserInfoPhone, + Address: idTokenData.Address, + Claims: idTokenData.Claims, + } + got := idTokenData.GetUserInfo() + assert.Equal(t, want, got) +} From c8cf15e26609b6fe63c9464ac9e6095dd34eabc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 17 Mar 2023 18:41:41 +0200 Subject: [PATCH 201/502] upgrade this module to v3 --- README.md | 8 ++++---- example/client/api/api.go | 4 ++-- example/client/app/app.go | 6 +++--- example/client/device/device.go | 4 ++-- example/client/github/github.go | 8 ++++---- example/client/service/service.go | 2 +- example/server/dynamic/login.go | 2 +- example/server/dynamic/op.go | 4 ++-- example/server/exampleop/device.go | 2 +- example/server/exampleop/op.go | 4 ++-- example/server/main.go | 4 ++-- example/server/storage/client.go | 4 ++-- example/server/storage/oidc.go | 4 ++-- example/server/storage/storage.go | 4 ++-- example/server/storage/storage_dynamic.go | 4 ++-- go.mod | 2 +- internal/testutil/gen/gen.go | 4 ++-- internal/testutil/token.go | 2 +- pkg/client/client.go | 6 +++--- pkg/client/integration_test.go | 14 +++++++------- pkg/client/jwt_profile.go | 4 ++-- pkg/client/profile/jwt_profile.go | 4 ++-- pkg/client/rp/cli/cli.go | 6 +++--- pkg/client/rp/delegation.go | 2 +- pkg/client/rp/device.go | 4 ++-- pkg/client/rp/jwks.go | 4 ++-- pkg/client/rp/relying_party.go | 6 +++--- pkg/client/rp/tockenexchange.go | 2 +- pkg/client/rp/verifier.go | 2 +- pkg/client/rp/verifier_test.go | 4 ++-- pkg/client/rp/verifier_tokens_example_test.go | 6 +++--- pkg/client/rs/resource_server.go | 6 +++--- pkg/client/tokenexchange/tokenexchange.go | 6 +++--- pkg/oidc/code_challenge.go | 2 +- pkg/oidc/token.go | 2 +- pkg/oidc/verifier.go | 2 +- pkg/op/auth_request.go | 6 +++--- pkg/op/auth_request_test.go | 8 ++++---- pkg/op/client.go | 4 ++-- pkg/op/client_test.go | 8 ++++---- pkg/op/crypto.go | 2 +- pkg/op/device.go | 4 ++-- pkg/op/device_test.go | 4 ++-- pkg/op/discovery.go | 4 ++-- pkg/op/discovery_test.go | 6 +++--- pkg/op/endpoint_test.go | 2 +- pkg/op/error.go | 4 ++-- pkg/op/keys.go | 2 +- pkg/op/keys_test.go | 6 +++--- pkg/op/mock/authorizer.mock.go | 6 +++--- pkg/op/mock/authorizer.mock.impl.go | 4 ++-- pkg/op/mock/client.go | 4 ++-- pkg/op/mock/client.mock.go | 6 +++--- pkg/op/mock/configuration.mock.go | 4 ++-- pkg/op/mock/discovery.mock.go | 2 +- pkg/op/mock/generate.go | 14 +++++++------- pkg/op/mock/key.mock.go | 4 ++-- pkg/op/mock/signer.mock.go | 2 +- pkg/op/mock/storage.mock.go | 6 +++--- pkg/op/mock/storage.mock.impl.go | 4 ++-- pkg/op/op.go | 4 ++-- pkg/op/op_test.go | 6 +++--- pkg/op/probes.go | 2 +- pkg/op/session.go | 4 ++-- pkg/op/storage.go | 2 +- pkg/op/token.go | 6 +++--- pkg/op/token_client_credentials.go | 4 ++-- pkg/op/token_code.go | 4 ++-- pkg/op/token_exchange.go | 4 ++-- pkg/op/token_intospection.go | 4 ++-- pkg/op/token_jwt_profile.go | 4 ++-- pkg/op/token_refresh.go | 6 +++--- pkg/op/token_request.go | 4 ++-- pkg/op/token_revocation.go | 4 ++-- pkg/op/userinfo.go | 4 ++-- pkg/op/verifier_access_token.go | 2 +- pkg/op/verifier_access_token_example_test.go | 6 +++--- pkg/op/verifier_access_token_test.go | 4 ++-- pkg/op/verifier_id_token_hint.go | 2 +- pkg/op/verifier_id_token_hint_test.go | 4 ++-- pkg/op/verifier_jwt_profile.go | 2 +- 81 files changed, 176 insertions(+), 176 deletions(-) diff --git a/README.md b/README.md index f369a5c..b7993e6 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,9 @@ Check the `/example` folder where example code for different scenarios is locate ```bash # start oidc op server # oidc discovery http://localhost:9998/.well-known/openid-configuration -go run github.com/zitadel/oidc/v2/example/server +go run github.com/zitadel/oidc/v3/example/server # start oidc web client (in a new terminal) -CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v2/example/client/app +CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v3/example/client/app ``` - open http://localhost:9999/login in your browser @@ -56,11 +56,11 @@ CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998/ SCOPES="openid for the dynamic issuer, just start it with: ```bash -go run github.com/zitadel/oidc/v2/example/server/dynamic +go run github.com/zitadel/oidc/v3/example/server/dynamic ``` the oidc web client above will still work, but if you add `oidc.local` (pointing to 127.0.0.1) in your hosts file you can also start it with: ```bash -CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v2/example/client/app +CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v3/example/client/app ``` > Note: Usernames are suffixed with the hostname (`test-user@localhost` or `test-user@oidc.local`) diff --git a/example/client/api/api.go b/example/client/api/api.go index 9f654a9..95e84e7 100644 --- a/example/client/api/api.go +++ b/example/client/api/api.go @@ -12,8 +12,8 @@ import ( "github.com/go-chi/chi" "github.com/sirupsen/logrus" - "github.com/zitadel/oidc/v2/pkg/client/rs" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rs" + "github.com/zitadel/oidc/v3/pkg/oidc" ) const ( diff --git a/example/client/app/app.go b/example/client/app/app.go index 0c324d2..446c17b 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -11,9 +11,9 @@ import ( "github.com/google/uuid" "github.com/sirupsen/logrus" - "github.com/zitadel/oidc/v2/pkg/client/rp" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) var ( diff --git a/example/client/device/device.go b/example/client/device/device.go index 284ba37..88ecfe9 100644 --- a/example/client/device/device.go +++ b/example/client/device/device.go @@ -11,8 +11,8 @@ import ( "github.com/sirupsen/logrus" - "github.com/zitadel/oidc/v2/pkg/client/rp" - httphelper "github.com/zitadel/oidc/v2/pkg/http" + "github.com/zitadel/oidc/v3/pkg/client/rp" + httphelper "github.com/zitadel/oidc/v3/pkg/http" ) var ( diff --git a/example/client/github/github.go b/example/client/github/github.go index 9cb813c..7d069d4 100644 --- a/example/client/github/github.go +++ b/example/client/github/github.go @@ -10,10 +10,10 @@ import ( "golang.org/x/oauth2" githubOAuth "golang.org/x/oauth2/github" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/client/rp/cli" - "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/client/rp/cli" + "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) var ( diff --git a/example/client/service/service.go b/example/client/service/service.go index 9526174..4908b09 100644 --- a/example/client/service/service.go +++ b/example/client/service/service.go @@ -13,7 +13,7 @@ import ( "github.com/sirupsen/logrus" "golang.org/x/oauth2" - "github.com/zitadel/oidc/v2/pkg/client/profile" + "github.com/zitadel/oidc/v3/pkg/client/profile" ) var client = http.DefaultClient diff --git a/example/server/dynamic/login.go b/example/server/dynamic/login.go index eb5340e..d90fb8e 100644 --- a/example/server/dynamic/login.go +++ b/example/server/dynamic/login.go @@ -8,7 +8,7 @@ import ( "github.com/go-chi/chi" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/op" ) const ( diff --git a/example/server/dynamic/op.go b/example/server/dynamic/op.go index 2bb6832..1662729 100644 --- a/example/server/dynamic/op.go +++ b/example/server/dynamic/op.go @@ -10,8 +10,8 @@ import ( "github.com/go-chi/chi" "golang.org/x/text/language" - "github.com/zitadel/oidc/v2/example/server/storage" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/example/server/storage" + "github.com/zitadel/oidc/v3/pkg/op" ) const ( diff --git a/example/server/exampleop/device.go b/example/server/exampleop/device.go index 59c2196..0dda3d5 100644 --- a/example/server/exampleop/device.go +++ b/example/server/exampleop/device.go @@ -10,7 +10,7 @@ import ( "github.com/go-chi/chi" "github.com/gorilla/securecookie" "github.com/sirupsen/logrus" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/op" ) type deviceAuthenticate interface { diff --git a/example/server/exampleop/op.go b/example/server/exampleop/op.go index 077244c..120590f 100644 --- a/example/server/exampleop/op.go +++ b/example/server/exampleop/op.go @@ -9,8 +9,8 @@ import ( "github.com/go-chi/chi" "golang.org/x/text/language" - "github.com/zitadel/oidc/v2/example/server/storage" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/example/server/storage" + "github.com/zitadel/oidc/v3/pkg/op" ) const ( diff --git a/example/server/main.go b/example/server/main.go index a2836ea..ee27bba 100644 --- a/example/server/main.go +++ b/example/server/main.go @@ -5,8 +5,8 @@ import ( "log" "net/http" - "github.com/zitadel/oidc/v2/example/server/exampleop" - "github.com/zitadel/oidc/v2/example/server/storage" + "github.com/zitadel/oidc/v3/example/server/exampleop" + "github.com/zitadel/oidc/v3/example/server/storage" ) func main() { diff --git a/example/server/storage/client.go b/example/server/storage/client.go index b850053..300ce0a 100644 --- a/example/server/storage/client.go +++ b/example/server/storage/client.go @@ -3,8 +3,8 @@ package storage import ( "time" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" ) var ( diff --git a/example/server/storage/oidc.go b/example/server/storage/oidc.go index f5412cf..b56ad09 100644 --- a/example/server/storage/oidc.go +++ b/example/server/storage/oidc.go @@ -5,8 +5,8 @@ import ( "golang.org/x/text/language" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" ) const ( diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index 7e1afbd..2aeefe7 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -14,8 +14,8 @@ import ( "github.com/google/uuid" "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" ) // serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant diff --git a/example/server/storage/storage_dynamic.go b/example/server/storage/storage_dynamic.go index 6e5ee32..3aec9d7 100644 --- a/example/server/storage/storage_dynamic.go +++ b/example/server/storage/storage_dynamic.go @@ -6,8 +6,8 @@ import ( "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" ) type multiStorage struct { diff --git a/go.mod b/go.mod index a636250..7cee26e 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/zitadel/oidc/v2 +module github.com/zitadel/oidc/v3 go 1.18 diff --git a/internal/testutil/gen/gen.go b/internal/testutil/gen/gen.go index a9f5925..e4a5718 100644 --- a/internal/testutil/gen/gen.go +++ b/internal/testutil/gen/gen.go @@ -8,8 +8,8 @@ import ( "fmt" "os" - tu "github.com/zitadel/oidc/v2/internal/testutil" - "github.com/zitadel/oidc/v2/pkg/oidc" + tu "github.com/zitadel/oidc/v3/internal/testutil" + "github.com/zitadel/oidc/v3/pkg/oidc" ) var custom = map[string]any{ diff --git a/internal/testutil/token.go b/internal/testutil/token.go index 121aa0b..27cab5d 100644 --- a/internal/testutil/token.go +++ b/internal/testutil/token.go @@ -8,7 +8,7 @@ import ( "errors" "time" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "gopkg.in/square/go-jose.v2" ) diff --git a/pkg/client/client.go b/pkg/client/client.go index 9eda973..e9af8ce 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -14,9 +14,9 @@ import ( "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/v2/pkg/crypto" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/crypto" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) var Encoder = httphelper.Encoder(oidc.NewEncoder()) diff --git a/pkg/client/integration_test.go b/pkg/client/integration_test.go index e19a720..709d5a1 100644 --- a/pkg/client/integration_test.go +++ b/pkg/client/integration_test.go @@ -18,13 +18,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/example/server/exampleop" - "github.com/zitadel/oidc/v2/example/server/storage" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/client/rs" - "github.com/zitadel/oidc/v2/pkg/client/tokenexchange" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/example/server/exampleop" + "github.com/zitadel/oidc/v3/example/server/storage" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/client/rs" + "github.com/zitadel/oidc/v3/pkg/client/tokenexchange" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) func TestRelyingPartySession(t *testing.T) { diff --git a/pkg/client/jwt_profile.go b/pkg/client/jwt_profile.go index 1686de6..486d998 100644 --- a/pkg/client/jwt_profile.go +++ b/pkg/client/jwt_profile.go @@ -5,8 +5,8 @@ import ( "golang.org/x/oauth2" - "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) // JWTProfileExchange handles the oauth2 jwt profile exchange diff --git a/pkg/client/profile/jwt_profile.go b/pkg/client/profile/jwt_profile.go index a934f7d..bb18570 100644 --- a/pkg/client/profile/jwt_profile.go +++ b/pkg/client/profile/jwt_profile.go @@ -7,8 +7,8 @@ import ( "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/v2/pkg/client" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client" + "github.com/zitadel/oidc/v3/pkg/oidc" ) // jwtProfileTokenSource implement the oauth2.TokenSource diff --git a/pkg/client/rp/cli/cli.go b/pkg/client/rp/cli/cli.go index 91b200d..eeb9011 100644 --- a/pkg/client/rp/cli/cli.go +++ b/pkg/client/rp/cli/cli.go @@ -4,9 +4,9 @@ import ( "context" "net/http" - "github.com/zitadel/oidc/v2/pkg/client/rp" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) const ( diff --git a/pkg/client/rp/delegation.go b/pkg/client/rp/delegation.go index b16a39e..23ecffd 100644 --- a/pkg/client/rp/delegation.go +++ b/pkg/client/rp/delegation.go @@ -1,7 +1,7 @@ package rp import ( - "github.com/zitadel/oidc/v2/pkg/oidc/grants/tokenexchange" + "github.com/zitadel/oidc/v3/pkg/oidc/grants/tokenexchange" ) // DelegationTokenRequest is an implementation of TokenExchangeRequest diff --git a/pkg/client/rp/device.go b/pkg/client/rp/device.go index 73b67ca..9cfc41e 100644 --- a/pkg/client/rp/device.go +++ b/pkg/client/rp/device.go @@ -5,8 +5,8 @@ import ( "fmt" "time" - "github.com/zitadel/oidc/v2/pkg/client" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client" + "github.com/zitadel/oidc/v3/pkg/oidc" ) func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.ClientCredentialsRequest, error) { diff --git a/pkg/client/rp/jwks.go b/pkg/client/rp/jwks.go index 3438bd6..79cf232 100644 --- a/pkg/client/rp/jwks.go +++ b/pkg/client/rp/jwks.go @@ -9,8 +9,8 @@ import ( "gopkg.in/square/go-jose.v2" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) func NewRemoteKeySet(client *http.Client, jwksURL string, opts ...func(*remoteKeySet)) oidc.KeySet { diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index ede7453..725715e 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -14,9 +14,9 @@ import ( "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/v2/pkg/client" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) const ( diff --git a/pkg/client/rp/tockenexchange.go b/pkg/client/rp/tockenexchange.go index c1ac88d..c8ca048 100644 --- a/pkg/client/rp/tockenexchange.go +++ b/pkg/client/rp/tockenexchange.go @@ -5,7 +5,7 @@ import ( "golang.org/x/oauth2" - "github.com/zitadel/oidc/v2/pkg/oidc/grants/tokenexchange" + "github.com/zitadel/oidc/v3/pkg/oidc/grants/tokenexchange" ) // TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange` diff --git a/pkg/client/rp/verifier.go b/pkg/client/rp/verifier.go index 75d149b..0cf427a 100644 --- a/pkg/client/rp/verifier.go +++ b/pkg/client/rp/verifier.go @@ -6,7 +6,7 @@ import ( "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" ) type IDTokenVerifier interface { diff --git a/pkg/client/rp/verifier_test.go b/pkg/client/rp/verifier_test.go index 7588c1f..002d65d 100644 --- a/pkg/client/rp/verifier_test.go +++ b/pkg/client/rp/verifier_test.go @@ -7,8 +7,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - tu "github.com/zitadel/oidc/v2/internal/testutil" - "github.com/zitadel/oidc/v2/pkg/oidc" + tu "github.com/zitadel/oidc/v3/internal/testutil" + "github.com/zitadel/oidc/v3/pkg/oidc" "gopkg.in/square/go-jose.v2" ) diff --git a/pkg/client/rp/verifier_tokens_example_test.go b/pkg/client/rp/verifier_tokens_example_test.go index c297efe..892eb23 100644 --- a/pkg/client/rp/verifier_tokens_example_test.go +++ b/pkg/client/rp/verifier_tokens_example_test.go @@ -4,9 +4,9 @@ import ( "context" "fmt" - tu "github.com/zitadel/oidc/v2/internal/testutil" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" + tu "github.com/zitadel/oidc/v3/internal/testutil" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" ) // MyCustomClaims extends the TokenClaims base, diff --git a/pkg/client/rs/resource_server.go b/pkg/client/rs/resource_server.go index 4e0353c..f0e0e0a 100644 --- a/pkg/client/rs/resource_server.go +++ b/pkg/client/rs/resource_server.go @@ -6,9 +6,9 @@ import ( "net/http" "time" - "github.com/zitadel/oidc/v2/pkg/client" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) type ResourceServer interface { diff --git a/pkg/client/tokenexchange/tokenexchange.go b/pkg/client/tokenexchange/tokenexchange.go index 1375f68..ce665cd 100644 --- a/pkg/client/tokenexchange/tokenexchange.go +++ b/pkg/client/tokenexchange/tokenexchange.go @@ -4,9 +4,9 @@ import ( "errors" "net/http" - "github.com/zitadel/oidc/v2/pkg/client" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) type TokenExchanger interface { diff --git a/pkg/oidc/code_challenge.go b/pkg/oidc/code_challenge.go index 37c1783..3296362 100644 --- a/pkg/oidc/code_challenge.go +++ b/pkg/oidc/code_challenge.go @@ -3,7 +3,7 @@ package oidc import ( "crypto/sha256" - "github.com/zitadel/oidc/v2/pkg/crypto" + "github.com/zitadel/oidc/v3/pkg/crypto" ) const ( diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index b017023..83f3805 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -8,7 +8,7 @@ import ( "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/v2/pkg/crypto" + "github.com/zitadel/oidc/v3/pkg/crypto" ) const ( diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go index c4ee95e..ad82617 100644 --- a/pkg/oidc/verifier.go +++ b/pkg/oidc/verifier.go @@ -12,7 +12,7 @@ import ( "gopkg.in/square/go-jose.v2" - str "github.com/zitadel/oidc/v2/pkg/strings" + str "github.com/zitadel/oidc/v3/pkg/strings" ) type Claims interface { diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 4c48363..1d1add5 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -11,9 +11,9 @@ import ( "strings" "time" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" - str "github.com/zitadel/oidc/v2/pkg/strings" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" + str "github.com/zitadel/oidc/v3/pkg/strings" ) type AuthRequest interface { diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index 542f2e2..65c65da 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -12,10 +12,10 @@ import ( "github.com/gorilla/schema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" - "github.com/zitadel/oidc/v2/pkg/op/mock" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" + "github.com/zitadel/oidc/v3/pkg/op/mock" ) // diff --git a/pkg/op/client.go b/pkg/op/client.go index af4724a..175caec 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -7,8 +7,8 @@ import ( "net/url" "time" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) //go:generate go get github.com/dmarkham/enumer diff --git a/pkg/op/client_test.go b/pkg/op/client_test.go index 1af4157..2e40d9a 100644 --- a/pkg/op/client_test.go +++ b/pkg/op/client_test.go @@ -14,10 +14,10 @@ import ( "github.com/gorilla/schema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" - "github.com/zitadel/oidc/v2/pkg/op/mock" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" + "github.com/zitadel/oidc/v3/pkg/op/mock" ) type testClientJWTProfile struct{} diff --git a/pkg/op/crypto.go b/pkg/op/crypto.go index 6786022..6ab1e0a 100644 --- a/pkg/op/crypto.go +++ b/pkg/op/crypto.go @@ -1,7 +1,7 @@ package op import ( - "github.com/zitadel/oidc/v2/pkg/crypto" + "github.com/zitadel/oidc/v3/pkg/crypto" ) type Crypto interface { diff --git a/pkg/op/device.go b/pkg/op/device.go index 04c06f2..e54da70 100644 --- a/pkg/op/device.go +++ b/pkg/op/device.go @@ -11,8 +11,8 @@ import ( "strings" "time" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) type DeviceAuthorizationConfig struct { diff --git a/pkg/op/device_test.go b/pkg/op/device_test.go index 69ba102..ab11700 100644 --- a/pkg/op/device_test.go +++ b/pkg/op/device_test.go @@ -15,8 +15,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" ) func Test_deviceAuthorizationHandler(t *testing.T) { diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 26f89eb..38afeab 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -6,8 +6,8 @@ import ( "gopkg.in/square/go-jose.v2" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) type DiscoverStorage interface { diff --git a/pkg/op/discovery_test.go b/pkg/op/discovery_test.go index 2d0b8af..e55e905 100644 --- a/pkg/op/discovery_test.go +++ b/pkg/op/discovery_test.go @@ -11,9 +11,9 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" - "github.com/zitadel/oidc/v2/pkg/op/mock" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" + "github.com/zitadel/oidc/v3/pkg/op/mock" ) func TestDiscover(t *testing.T) { diff --git a/pkg/op/endpoint_test.go b/pkg/op/endpoint_test.go index 50de89c..46e5d47 100644 --- a/pkg/op/endpoint_test.go +++ b/pkg/op/endpoint_test.go @@ -3,7 +3,7 @@ package op_test import ( "testing" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/op" ) func TestEndpoint_Path(t *testing.T) { diff --git a/pkg/op/error.go b/pkg/op/error.go index acca4ab..b2d84ae 100644 --- a/pkg/op/error.go +++ b/pkg/op/error.go @@ -3,8 +3,8 @@ package op import ( "net/http" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) type ErrAuthRequest interface { diff --git a/pkg/op/keys.go b/pkg/op/keys.go index 239ecbd..418dcb5 100644 --- a/pkg/op/keys.go +++ b/pkg/op/keys.go @@ -6,7 +6,7 @@ import ( "gopkg.in/square/go-jose.v2" - httphelper "github.com/zitadel/oidc/v2/pkg/http" + httphelper "github.com/zitadel/oidc/v3/pkg/http" ) type KeyProvider interface { diff --git a/pkg/op/keys_test.go b/pkg/op/keys_test.go index 2e56b78..259b87c 100644 --- a/pkg/op/keys_test.go +++ b/pkg/op/keys_test.go @@ -11,9 +11,9 @@ import ( "github.com/stretchr/testify/assert" "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" - "github.com/zitadel/oidc/v2/pkg/op/mock" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" + "github.com/zitadel/oidc/v3/pkg/op/mock" ) func TestKeys(t *testing.T) { diff --git a/pkg/op/mock/authorizer.mock.go b/pkg/op/mock/authorizer.mock.go index cc913ee..931b896 100644 --- a/pkg/op/mock/authorizer.mock.go +++ b/pkg/op/mock/authorizer.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: Authorizer) +// Source: github.com/zitadel/oidc/v3/pkg/op (interfaces: Authorizer) // Package mock is a generated GoMock package. package mock @@ -9,8 +9,8 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - http "github.com/zitadel/oidc/v2/pkg/http" - op "github.com/zitadel/oidc/v2/pkg/op" + http "github.com/zitadel/oidc/v3/pkg/http" + op "github.com/zitadel/oidc/v3/pkg/op" ) // MockAuthorizer is a mock of Authorizer interface. diff --git a/pkg/op/mock/authorizer.mock.impl.go b/pkg/op/mock/authorizer.mock.impl.go index 3f1d525..6a5bdfd 100644 --- a/pkg/op/mock/authorizer.mock.impl.go +++ b/pkg/op/mock/authorizer.mock.impl.go @@ -8,8 +8,8 @@ import ( "github.com/gorilla/schema" "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" ) func NewAuthorizer(t *testing.T) op.Authorizer { diff --git a/pkg/op/mock/client.go b/pkg/op/mock/client.go index 36df84a..f01e3ec 100644 --- a/pkg/op/mock/client.go +++ b/pkg/op/mock/client.go @@ -5,8 +5,8 @@ import ( "github.com/golang/mock/gomock" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" ) func NewClient(t *testing.T) op.Client { diff --git a/pkg/op/mock/client.mock.go b/pkg/op/mock/client.mock.go index e3d19fb..9be0807 100644 --- a/pkg/op/mock/client.mock.go +++ b/pkg/op/mock/client.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: Client) +// Source: github.com/zitadel/oidc/v3/pkg/op (interfaces: Client) // Package mock is a generated GoMock package. package mock @@ -9,8 +9,8 @@ import ( time "time" gomock "github.com/golang/mock/gomock" - oidc "github.com/zitadel/oidc/v2/pkg/oidc" - op "github.com/zitadel/oidc/v2/pkg/op" + oidc "github.com/zitadel/oidc/v3/pkg/oidc" + op "github.com/zitadel/oidc/v3/pkg/op" ) // MockClient is a mock of Client interface. diff --git a/pkg/op/mock/configuration.mock.go b/pkg/op/mock/configuration.mock.go index fe7d4da..96429dd 100644 --- a/pkg/op/mock/configuration.mock.go +++ b/pkg/op/mock/configuration.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: Configuration) +// Source: github.com/zitadel/oidc/v3/pkg/op (interfaces: Configuration) // Package mock is a generated GoMock package. package mock @@ -9,7 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - op "github.com/zitadel/oidc/v2/pkg/op" + op "github.com/zitadel/oidc/v3/pkg/op" language "golang.org/x/text/language" ) diff --git a/pkg/op/mock/discovery.mock.go b/pkg/op/mock/discovery.mock.go index 0c78d52..4c33953 100644 --- a/pkg/op/mock/discovery.mock.go +++ b/pkg/op/mock/discovery.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: DiscoverStorage) +// Source: github.com/zitadel/oidc/v3/pkg/op (interfaces: DiscoverStorage) // Package mock is a generated GoMock package. package mock diff --git a/pkg/op/mock/generate.go b/pkg/op/mock/generate.go index ca288d2..590356c 100644 --- a/pkg/op/mock/generate.go +++ b/pkg/op/mock/generate.go @@ -1,10 +1,10 @@ package mock //go:generate go install github.com/golang/mock/mockgen@v1.6.0 -//go:generate mockgen -package mock -destination ./storage.mock.go github.com/zitadel/oidc/v2/pkg/op Storage -//go:generate mockgen -package mock -destination ./authorizer.mock.go github.com/zitadel/oidc/v2/pkg/op Authorizer -//go:generate mockgen -package mock -destination ./client.mock.go github.com/zitadel/oidc/v2/pkg/op Client -//go:generate mockgen -package mock -destination ./configuration.mock.go github.com/zitadel/oidc/v2/pkg/op Configuration -//go:generate mockgen -package mock -destination ./discovery.mock.go github.com/zitadel/oidc/v2/pkg/op DiscoverStorage -//go:generate mockgen -package mock -destination ./signer.mock.go github.com/zitadel/oidc/v2/pkg/op SigningKey,Key -//go:generate mockgen -package mock -destination ./key.mock.go github.com/zitadel/oidc/v2/pkg/op KeyProvider +//go:generate mockgen -package mock -destination ./storage.mock.go github.com/zitadel/oidc/v3/pkg/op Storage +//go:generate mockgen -package mock -destination ./authorizer.mock.go github.com/zitadel/oidc/v3/pkg/op Authorizer +//go:generate mockgen -package mock -destination ./client.mock.go github.com/zitadel/oidc/v3/pkg/op Client +//go:generate mockgen -package mock -destination ./configuration.mock.go github.com/zitadel/oidc/v3/pkg/op Configuration +//go:generate mockgen -package mock -destination ./discovery.mock.go github.com/zitadel/oidc/v3/pkg/op DiscoverStorage +//go:generate mockgen -package mock -destination ./signer.mock.go github.com/zitadel/oidc/v3/pkg/op SigningKey,Key +//go:generate mockgen -package mock -destination ./key.mock.go github.com/zitadel/oidc/v3/pkg/op KeyProvider diff --git a/pkg/op/mock/key.mock.go b/pkg/op/mock/key.mock.go index 8831651..122e852 100644 --- a/pkg/op/mock/key.mock.go +++ b/pkg/op/mock/key.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: KeyProvider) +// Source: github.com/zitadel/oidc/v3/pkg/op (interfaces: KeyProvider) // Package mock is a generated GoMock package. package mock @@ -9,7 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - op "github.com/zitadel/oidc/v2/pkg/op" + op "github.com/zitadel/oidc/v3/pkg/op" ) // MockKeyProvider is a mock of KeyProvider interface. diff --git a/pkg/op/mock/signer.mock.go b/pkg/op/mock/signer.mock.go index 78c0efe..7075241 100644 --- a/pkg/op/mock/signer.mock.go +++ b/pkg/op/mock/signer.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: SigningKey,Key) +// Source: github.com/zitadel/oidc/v3/pkg/op (interfaces: SigningKey,Key) // Package mock is a generated GoMock package. package mock diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index 85afb2a..6bfb1c9 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: Storage) +// Source: github.com/zitadel/oidc/v3/pkg/op (interfaces: Storage) // Package mock is a generated GoMock package. package mock @@ -10,8 +10,8 @@ import ( time "time" gomock "github.com/golang/mock/gomock" - oidc "github.com/zitadel/oidc/v2/pkg/oidc" - op "github.com/zitadel/oidc/v2/pkg/op" + oidc "github.com/zitadel/oidc/v3/pkg/oidc" + op "github.com/zitadel/oidc/v3/pkg/op" jose "gopkg.in/square/go-jose.v2" ) diff --git a/pkg/op/mock/storage.mock.impl.go b/pkg/op/mock/storage.mock.impl.go index 9269f89..002da7e 100644 --- a/pkg/op/mock/storage.mock.impl.go +++ b/pkg/op/mock/storage.mock.impl.go @@ -8,8 +8,8 @@ import ( "github.com/golang/mock/gomock" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" ) func NewStorage(t *testing.T) op.Storage { diff --git a/pkg/op/op.go b/pkg/op/op.go index 0536bbc..27c1410 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -12,8 +12,8 @@ import ( "golang.org/x/text/language" "gopkg.in/square/go-jose.v2" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) const ( diff --git a/pkg/op/op_test.go b/pkg/op/op_test.go index 8429212..3958b89 100644 --- a/pkg/op/op_test.go +++ b/pkg/op/op_test.go @@ -14,9 +14,9 @@ import ( "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/example/server/storage" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/example/server/storage" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" "golang.org/x/text/language" ) diff --git a/pkg/op/probes.go b/pkg/op/probes.go index a56c92b..9ef5bb5 100644 --- a/pkg/op/probes.go +++ b/pkg/op/probes.go @@ -5,7 +5,7 @@ import ( "errors" "net/http" - httphelper "github.com/zitadel/oidc/v2/pkg/http" + httphelper "github.com/zitadel/oidc/v3/pkg/http" ) type ProbesFn func(context.Context) error diff --git a/pkg/op/session.go b/pkg/op/session.go index c4f76f3..fbce125 100644 --- a/pkg/op/session.go +++ b/pkg/op/session.go @@ -6,8 +6,8 @@ import ( "net/url" "path" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) type SessionEnder interface { diff --git a/pkg/op/storage.go b/pkg/op/storage.go index e36eac7..25444dd 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -7,7 +7,7 @@ import ( "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" ) type AuthStorage interface { diff --git a/pkg/op/token.go b/pkg/op/token.go index 58568a7..44648aa 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -4,9 +4,9 @@ import ( "context" "time" - "github.com/zitadel/oidc/v2/pkg/crypto" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/strings" + "github.com/zitadel/oidc/v3/pkg/crypto" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/strings" ) type TokenCreator interface { diff --git a/pkg/op/token_client_credentials.go b/pkg/op/token_client_credentials.go index fc31d57..0cf7796 100644 --- a/pkg/op/token_client_credentials.go +++ b/pkg/op/token_client_credentials.go @@ -5,8 +5,8 @@ import ( "net/http" "net/url" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) // ClientCredentialsExchange handles the OAuth 2.0 client_credentials grant, including diff --git a/pkg/op/token_code.go b/pkg/op/token_code.go index 565a477..b5e892a 100644 --- a/pkg/op/token_code.go +++ b/pkg/op/token_code.go @@ -4,8 +4,8 @@ import ( "context" "net/http" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) // CodeExchange handles the OAuth 2.0 authorization_code grant, including diff --git a/pkg/op/token_exchange.go b/pkg/op/token_exchange.go index 055ff13..93aa9b2 100644 --- a/pkg/op/token_exchange.go +++ b/pkg/op/token_exchange.go @@ -7,8 +7,8 @@ import ( "strings" "time" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) type TokenExchangeRequest interface { diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go index 8582388..28df217 100644 --- a/pkg/op/token_intospection.go +++ b/pkg/op/token_intospection.go @@ -5,8 +5,8 @@ import ( "errors" "net/http" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) type Introspector interface { diff --git a/pkg/op/token_jwt_profile.go b/pkg/op/token_jwt_profile.go index 23bac9a..4563e16 100644 --- a/pkg/op/token_jwt_profile.go +++ b/pkg/op/token_jwt_profile.go @@ -5,8 +5,8 @@ import ( "net/http" "time" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) type JWTAuthorizationGrantExchanger interface { diff --git a/pkg/op/token_refresh.go b/pkg/op/token_refresh.go index 148d2a4..aeaa5b4 100644 --- a/pkg/op/token_refresh.go +++ b/pkg/op/token_refresh.go @@ -6,9 +6,9 @@ import ( "net/http" "time" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/strings" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/strings" ) type RefreshTokenRequest interface { diff --git a/pkg/op/token_request.go b/pkg/op/token_request.go index b9e9805..058a202 100644 --- a/pkg/op/token_request.go +++ b/pkg/op/token_request.go @@ -5,8 +5,8 @@ import ( "net/http" "net/url" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) type Exchanger interface { diff --git a/pkg/op/token_revocation.go b/pkg/op/token_revocation.go index 58332c3..34f8746 100644 --- a/pkg/op/token_revocation.go +++ b/pkg/op/token_revocation.go @@ -7,8 +7,8 @@ import ( "net/url" "strings" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) type Revoker interface { diff --git a/pkg/op/userinfo.go b/pkg/op/userinfo.go index 21a0af4..52a2aa2 100644 --- a/pkg/op/userinfo.go +++ b/pkg/op/userinfo.go @@ -6,8 +6,8 @@ import ( "net/http" "strings" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" ) type UserinfoProvider interface { diff --git a/pkg/op/verifier_access_token.go b/pkg/op/verifier_access_token.go index 9a8b912..7527ea6 100644 --- a/pkg/op/verifier_access_token.go +++ b/pkg/op/verifier_access_token.go @@ -4,7 +4,7 @@ import ( "context" "time" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" ) type AccessTokenVerifier interface { diff --git a/pkg/op/verifier_access_token_example_test.go b/pkg/op/verifier_access_token_example_test.go index effdd58..397a2d3 100644 --- a/pkg/op/verifier_access_token_example_test.go +++ b/pkg/op/verifier_access_token_example_test.go @@ -4,9 +4,9 @@ import ( "context" "fmt" - tu "github.com/zitadel/oidc/v2/internal/testutil" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + tu "github.com/zitadel/oidc/v3/internal/testutil" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" ) // MyCustomClaims extends the TokenClaims base, diff --git a/pkg/op/verifier_access_token_test.go b/pkg/op/verifier_access_token_test.go index 62c26a9..a1972f1 100644 --- a/pkg/op/verifier_access_token_test.go +++ b/pkg/op/verifier_access_token_test.go @@ -7,8 +7,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - tu "github.com/zitadel/oidc/v2/internal/testutil" - "github.com/zitadel/oidc/v2/pkg/oidc" + tu "github.com/zitadel/oidc/v3/internal/testutil" + "github.com/zitadel/oidc/v3/pkg/oidc" ) func TestNewAccessTokenVerifier(t *testing.T) { diff --git a/pkg/op/verifier_id_token_hint.go b/pkg/op/verifier_id_token_hint.go index d906075..50c3ff6 100644 --- a/pkg/op/verifier_id_token_hint.go +++ b/pkg/op/verifier_id_token_hint.go @@ -4,7 +4,7 @@ import ( "context" "time" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" ) type IDTokenHintVerifier interface { diff --git a/pkg/op/verifier_id_token_hint_test.go b/pkg/op/verifier_id_token_hint_test.go index f4d0b0c..9f4c6c1 100644 --- a/pkg/op/verifier_id_token_hint_test.go +++ b/pkg/op/verifier_id_token_hint_test.go @@ -7,8 +7,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - tu "github.com/zitadel/oidc/v2/internal/testutil" - "github.com/zitadel/oidc/v2/pkg/oidc" + tu "github.com/zitadel/oidc/v3/internal/testutil" + "github.com/zitadel/oidc/v3/pkg/oidc" ) func TestNewIDTokenHintVerifier(t *testing.T) { diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index 4d83c59..b7dfec7 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -8,7 +8,7 @@ import ( "gopkg.in/square/go-jose.v2" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" ) type JWTProfileVerifier interface { From 115813ee38b74322d6a974163ff2e93a56aaa686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Mon, 20 Mar 2023 14:56:06 +0200 Subject: [PATCH 202/502] fix: handle the zero cases for oidc.Time --- pkg/oidc/types.go | 6 +++++ pkg/oidc/types_test.go | 51 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index cb513a0..167f8b7 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -173,10 +173,16 @@ func NewEncoder() *schema.Encoder { type Time int64 func (ts Time) AsTime() time.Time { + if ts == 0 { + return time.Time{} + } return time.Unix(int64(ts), 0) } func FromTime(tt time.Time) Time { + if tt.IsZero() { + return 0 + } return Time(tt.Unix()) } diff --git a/pkg/oidc/types_test.go b/pkg/oidc/types_test.go index 2721e0b..64f07f1 100644 --- a/pkg/oidc/types_test.go +++ b/pkg/oidc/types_test.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" "testing" + "time" "github.com/gorilla/schema" "github.com/stretchr/testify/assert" @@ -467,6 +468,56 @@ func TestNewEncoder(t *testing.T) { assert.Equal(t, a, b) } +func TestTime_AsTime(t *testing.T) { + tests := []struct { + name string + ts Time + want time.Time + }{ + { + name: "unset", + ts: 0, + want: time.Time{}, + }, + { + name: "set", + ts: 1, + want: time.Unix(1, 0), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.ts.AsTime() + assert.Equal(t, tt.want, got) + }) + } +} + +func TestTime_FromTime(t *testing.T) { + tests := []struct { + name string + tt time.Time + want Time + }{ + { + name: "zero", + tt: time.Time{}, + want: 0, + }, + { + name: "set", + tt: time.Unix(1, 0), + want: 1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := FromTime(tt.tt) + assert.Equal(t, tt.want, got) + }) + } +} + func TestTime_UnmarshalJSON(t *testing.T) { type dst struct { UpdatedAt Time `json:"updated_at"` From 3c1e81e6a673a41ee32d11314064ffde6d2c532b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Mar 2023 19:59:14 +0000 Subject: [PATCH 203/502] chore(deps): bump actions/setup-go from 3 to 4 Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-go dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2abef36..78c0f79 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Setup go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... From a08ce5009112da5ee4c8db01dcd51a871c0b934e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 21 Mar 2023 11:43:38 +0200 Subject: [PATCH 204/502] fix: correct returned field for JWTTokenRequest JWTTokenRequest.GetIssuedAt() was returning the ExpiresAt field. This change corrects that by returning IssuedAt instead. This bug was introduced in #283 --- pkg/oidc/token_request.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index e63e0e5..6b6945a 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -192,7 +192,7 @@ func (j *JWTTokenRequest) GetExpiration() time.Time { // GetIssuedAt implements the Claims interface func (j *JWTTokenRequest) GetIssuedAt() time.Time { - return j.ExpiresAt.AsTime() + return j.IssuedAt.AsTime() } // GetNonce implements the Claims interface From 33c716ddcfef94c88945bc268d89840cb7caff6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 22 Mar 2023 19:18:41 +0200 Subject: [PATCH 205/502] feat: merge the verifier types (#336) BREAKING CHANGE: - The various verifier types are merged into a oidc.Verifir. - oidc.Verfier became a struct with exported fields * use type aliases for oidc.Verifier this binds the correct contstructor to each verifier usecase. * fix: handle the zero cases for oidc.Time * add unit tests to oidc verifier * fix: correct returned field for JWTTokenRequest JWTTokenRequest.GetIssuedAt() was returning the ExpiresAt field. This change corrects that by returning IssuedAt instead. --- internal/testutil/token.go | 36 ++- pkg/client/rp/relying_party.go | 8 +- pkg/client/rp/verifier.go | 127 +++------ pkg/client/rp/verifier_test.go | 62 ++--- pkg/oidc/token_request.go | 2 +- pkg/oidc/types.go | 6 + pkg/oidc/types_test.go | 51 ++++ pkg/oidc/verifier.go | 36 ++- pkg/oidc/verifier_parse_test.go | 128 +++++++++ pkg/oidc/verifier_test.go | 374 ++++++++++++++++++++++++++ pkg/op/auth_request.go | 8 +- pkg/op/auth_request_test.go | 34 ++- pkg/op/client.go | 2 +- pkg/op/client_test.go | 2 +- pkg/op/mock/authorizer.mock.go | 4 +- pkg/op/mock/authorizer.mock.impl.go | 2 +- pkg/op/op.go | 10 +- pkg/op/session.go | 2 +- pkg/op/token_intospection.go | 2 +- pkg/op/token_jwt_profile.go | 2 +- pkg/op/token_request.go | 4 +- pkg/op/token_revocation.go | 4 +- pkg/op/userinfo.go | 2 +- pkg/op/verifier_access_token.go | 63 +---- pkg/op/verifier_access_token_test.go | 28 +- pkg/op/verifier_id_token_hint.go | 75 ++---- pkg/op/verifier_id_token_hint_test.go | 32 +-- pkg/op/verifier_jwt_profile.go | 76 ++---- pkg/op/verifier_jwt_profile_test.go | 117 ++++++++ 29 files changed, 948 insertions(+), 351 deletions(-) create mode 100644 pkg/oidc/verifier_parse_test.go create mode 100644 pkg/oidc/verifier_test.go create mode 100644 pkg/op/verifier_jwt_profile_test.go diff --git a/internal/testutil/token.go b/internal/testutil/token.go index 27cab5d..41778de 100644 --- a/internal/testutil/token.go +++ b/internal/testutil/token.go @@ -8,6 +8,7 @@ import ( "errors" "time" + "github.com/muhlemmer/gu" "github.com/zitadel/oidc/v3/pkg/oidc" "gopkg.in/square/go-jose.v2" ) @@ -17,7 +18,7 @@ type KeySet struct{} // VerifySignature implments op.KeySet. func (KeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) { - if ctx.Err() != nil { + if err = ctx.Err(); err != nil { return nil, err } @@ -45,6 +46,16 @@ func init() { } } +type JWTProfileKeyStorage struct{} + +func (JWTProfileKeyStorage) GetKeyByIDAndClientID(ctx context.Context, keyID string, clientID string) (*jose.JSONWebKey, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + + return gu.Ptr(WebKey.Public()), nil +} + func signEncodeTokenClaims(claims any) string { payload, err := json.Marshal(claims) if err != nil { @@ -106,6 +117,25 @@ func NewAccessToken(issuer, subject string, audience []string, expiration time.T return NewAccessTokenCustom(issuer, subject, audience, expiration, jwtid, clientID, skew, nil) } +func NewJWTProfileAssertion(issuer, clientID string, audience []string, issuedAt, expiration time.Time) (string, *oidc.JWTTokenRequest) { + req := &oidc.JWTTokenRequest{ + Issuer: issuer, + Subject: clientID, + Audience: audience, + ExpiresAt: oidc.FromTime(expiration), + IssuedAt: oidc.FromTime(issuedAt), + } + // make sure the private claim map is set correctly + data, err := json.Marshal(req) + if err != nil { + panic(err) + } + if err = json.Unmarshal(data, req); err != nil { + panic(err) + } + return signEncodeTokenClaims(req), req +} + const InvalidSignatureToken = `eyJhbGciOiJQUzUxMiJ9.eyJpc3MiOiJsb2NhbC5jb20iLCJzdWIiOiJ0aW1AbG9jYWwuY29tIiwiYXVkIjpbInVuaXQiLCJ0ZXN0IiwiNTU1NjY2Il0sImV4cCI6MTY3Nzg0MDQzMSwiaWF0IjoxNjc3ODQwMzcwLCJhdXRoX3RpbWUiOjE2Nzc4NDAzMTAsIm5vbmNlIjoiMTIzNDUiLCJhY3IiOiJzb21ldGhpbmciLCJhbXIiOlsiZm9vIiwiYmFyIl0sImF6cCI6IjU1NTY2NiJ9.DtZmvVkuE4Hw48ijBMhRJbxEWCr_WEYuPQBMY73J9TP6MmfeNFkjVJf4nh4omjB9gVLnQ-xhEkNOe62FS5P0BB2VOxPuHZUj34dNspCgG3h98fGxyiMb5vlIYAHDF9T-w_LntlYItohv63MmdYR-hPpAqjXE7KOfErf-wUDGE9R3bfiQ4HpTdyFJB1nsToYrZ9lhP2mzjTCTs58ckZfQ28DFHn_lfHWpR4rJBgvLx7IH4rMrUayr09Ap-PxQLbv0lYMtmgG1z3JK8MXnuYR0UJdZnEIezOzUTlThhCXB-nvuAXYjYxZZTR0FtlgZUHhIpYK0V2abf_Q_Or36akNCUg` // These variables always result in a valid token @@ -137,6 +167,10 @@ func ValidAccessToken() (string, *oidc.AccessTokenClaims) { return NewAccessToken(ValidIssuer, ValidSubject, ValidAudience, ValidExpiration, ValidJWTID, ValidClientID, ValidSkew) } +func ValidJWTProfileAssertion() (string, *oidc.JWTTokenRequest) { + return NewJWTProfileAssertion(ValidClientID, ValidClientID, []string{ValidIssuer}, time.Now(), ValidExpiration) +} + // ACRVerify is a oidc.ACRVerifier func. func ACRVerify(acr string) error { if acr != ValidACR { diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 725715e..bd96e16 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -63,8 +63,8 @@ type RelyingParty interface { // be used to start a DeviceAuthorization flow. GetDeviceAuthorizationEndpoint() string - // IDTokenVerifier returns the verifier interface used for oidc id_token verification - IDTokenVerifier() IDTokenVerifier + // IDTokenVerifier returns the verifier used for oidc id_token verification + IDTokenVerifier() *IDTokenVerifier // ErrorHandler returns the handler used for callback errors ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) @@ -88,7 +88,7 @@ type relyingParty struct { cookieHandler *httphelper.CookieHandler errorHandler func(http.ResponseWriter, *http.Request, string, string, string) - idTokenVerifier IDTokenVerifier + idTokenVerifier *IDTokenVerifier verifierOpts []VerifierOption signer jose.Signer } @@ -137,7 +137,7 @@ func (rp *relyingParty) GetRevokeEndpoint() string { return rp.endpoints.RevokeURL } -func (rp *relyingParty) IDTokenVerifier() IDTokenVerifier { +func (rp *relyingParty) IDTokenVerifier() *IDTokenVerifier { if rp.idTokenVerifier == nil { rp.idTokenVerifier = NewIDTokenVerifier(rp.issuer, rp.oauthConfig.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...) } diff --git a/pkg/client/rp/verifier.go b/pkg/client/rp/verifier.go index 0cf427a..3294f40 100644 --- a/pkg/client/rp/verifier.go +++ b/pkg/client/rp/verifier.go @@ -9,19 +9,9 @@ import ( "github.com/zitadel/oidc/v3/pkg/oidc" ) -type IDTokenVerifier interface { - oidc.Verifier - ClientID() string - SupportedSignAlgs() []string - KeySet() oidc.KeySet - Nonce(context.Context) string - ACR() oidc.ACRVerifier - MaxAge() time.Duration -} - // VerifyTokens implement the Token Response Validation as defined in OIDC specification // https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation -func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken string, v IDTokenVerifier) (claims C, err error) { +func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken string, v *IDTokenVerifier) (claims C, err error) { var nilClaims C claims, err = VerifyIDToken[C](ctx, idToken, v) @@ -36,7 +26,7 @@ func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken str // VerifyIDToken validates the id token according to // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation -func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v IDTokenVerifier) (claims C, err error) { +func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v *IDTokenVerifier) (claims C, err error) { var nilClaims C decrypted, err := oidc.DecryptToken(token) @@ -52,27 +42,27 @@ func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v IDTokenVe return nilClaims, err } - if err = oidc.CheckIssuer(claims, v.Issuer()); err != nil { + if err = oidc.CheckIssuer(claims, v.Issuer); err != nil { return nilClaims, err } - if err = oidc.CheckAudience(claims, v.ClientID()); err != nil { + if err = oidc.CheckAudience(claims, v.ClientID); err != nil { return nilClaims, err } - if err = oidc.CheckAuthorizedParty(claims, v.ClientID()); err != nil { + if err = oidc.CheckAuthorizedParty(claims, v.ClientID); err != nil { return nilClaims, err } - if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil { + if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs, v.KeySet); err != nil { return nilClaims, err } - if err = oidc.CheckExpiration(claims, v.Offset()); err != nil { + if err = oidc.CheckExpiration(claims, v.Offset); err != nil { return nilClaims, err } - if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT(), v.Offset()); err != nil { + if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT, v.Offset); err != nil { return nilClaims, err } @@ -80,16 +70,18 @@ func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v IDTokenVe return nilClaims, err } - if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR()); err != nil { + if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR); err != nil { return nilClaims, err } - if err = oidc.CheckAuthTime(claims, v.MaxAge()); err != nil { + if err = oidc.CheckAuthTime(claims, v.MaxAge); err != nil { return nilClaims, err } return claims, nil } +type IDTokenVerifier oidc.Verifier + // VerifyAccessToken validates the access token according to // https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAlgorithm) error { @@ -107,15 +99,14 @@ func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAl return nil } -// NewIDTokenVerifier returns an implementation of `IDTokenVerifier` -// for `VerifyTokens` and `VerifyIDToken` -func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ...VerifierOption) IDTokenVerifier { - v := &idTokenVerifier{ - issuer: issuer, - clientID: clientID, - keySet: keySet, - offset: time.Second, - nonce: func(_ context.Context) string { +// NewIDTokenVerifier returns a oidc.Verifier suitable for ID token verification. +func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ...VerifierOption) *IDTokenVerifier { + v := &IDTokenVerifier{ + Issuer: issuer, + ClientID: clientID, + KeySet: keySet, + Offset: time.Second, + Nonce: func(_ context.Context) string { return "" }, } @@ -128,95 +119,47 @@ func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ... } // VerifierOption is the type for providing dynamic options to the IDTokenVerifier -type VerifierOption func(*idTokenVerifier) +type VerifierOption func(*IDTokenVerifier) // WithIssuedAtOffset mitigates the risk of iat to be in the future // because of clock skews with the ability to add an offset to the current time -func WithIssuedAtOffset(offset time.Duration) func(*idTokenVerifier) { - return func(v *idTokenVerifier) { - v.offset = offset +func WithIssuedAtOffset(offset time.Duration) VerifierOption { + return func(v *IDTokenVerifier) { + v.Offset = offset } } // WithIssuedAtMaxAge provides the ability to define the maximum duration between iat and now -func WithIssuedAtMaxAge(maxAge time.Duration) func(*idTokenVerifier) { - return func(v *idTokenVerifier) { - v.maxAgeIAT = maxAge +func WithIssuedAtMaxAge(maxAge time.Duration) VerifierOption { + return func(v *IDTokenVerifier) { + v.MaxAgeIAT = maxAge } } // WithNonce sets the function to check the nonce func WithNonce(nonce func(context.Context) string) VerifierOption { - return func(v *idTokenVerifier) { - v.nonce = nonce + return func(v *IDTokenVerifier) { + v.Nonce = nonce } } // WithACRVerifier sets the verifier for the acr claim func WithACRVerifier(verifier oidc.ACRVerifier) VerifierOption { - return func(v *idTokenVerifier) { - v.acr = verifier + return func(v *IDTokenVerifier) { + v.ACR = verifier } } // WithAuthTimeMaxAge provides the ability to define the maximum duration between auth_time and now func WithAuthTimeMaxAge(maxAge time.Duration) VerifierOption { - return func(v *idTokenVerifier) { - v.maxAge = maxAge + return func(v *IDTokenVerifier) { + v.MaxAge = maxAge } } // WithSupportedSigningAlgorithms overwrites the default RS256 signing algorithm func WithSupportedSigningAlgorithms(algs ...string) VerifierOption { - return func(v *idTokenVerifier) { - v.supportedSignAlgs = algs + return func(v *IDTokenVerifier) { + v.SupportedSignAlgs = algs } } - -type idTokenVerifier struct { - issuer string - maxAgeIAT time.Duration - offset time.Duration - clientID string - supportedSignAlgs []string - keySet oidc.KeySet - acr oidc.ACRVerifier - maxAge time.Duration - nonce func(ctx context.Context) string -} - -func (i *idTokenVerifier) Issuer() string { - return i.issuer -} - -func (i *idTokenVerifier) MaxAgeIAT() time.Duration { - return i.maxAgeIAT -} - -func (i *idTokenVerifier) Offset() time.Duration { - return i.offset -} - -func (i *idTokenVerifier) ClientID() string { - return i.clientID -} - -func (i *idTokenVerifier) SupportedSignAlgs() []string { - return i.supportedSignAlgs -} - -func (i *idTokenVerifier) KeySet() oidc.KeySet { - return i.keySet -} - -func (i *idTokenVerifier) Nonce(ctx context.Context) string { - return i.nonce(ctx) -} - -func (i *idTokenVerifier) ACR() oidc.ACRVerifier { - return i.acr -} - -func (i *idTokenVerifier) MaxAge() time.Duration { - return i.maxAge -} diff --git a/pkg/client/rp/verifier_test.go b/pkg/client/rp/verifier_test.go index 002d65d..11bf2f9 100644 --- a/pkg/client/rp/verifier_test.go +++ b/pkg/client/rp/verifier_test.go @@ -13,16 +13,16 @@ import ( ) func TestVerifyTokens(t *testing.T) { - verifier := &idTokenVerifier{ - issuer: tu.ValidIssuer, - maxAgeIAT: 2 * time.Minute, - offset: time.Second, - supportedSignAlgs: []string{string(tu.SignatureAlgorithm)}, - keySet: tu.KeySet{}, - maxAge: 2 * time.Minute, - acr: tu.ACRVerify, - nonce: func(context.Context) string { return tu.ValidNonce }, - clientID: tu.ValidClientID, + verifier := &IDTokenVerifier{ + Issuer: tu.ValidIssuer, + MaxAgeIAT: 2 * time.Minute, + Offset: time.Second, + SupportedSignAlgs: []string{string(tu.SignatureAlgorithm)}, + KeySet: tu.KeySet{}, + MaxAge: 2 * time.Minute, + ACR: tu.ACRVerify, + Nonce: func(context.Context) string { return tu.ValidNonce }, + ClientID: tu.ValidClientID, } accessToken, _ := tu.ValidAccessToken() atHash, err := oidc.ClaimHash(accessToken, tu.SignatureAlgorithm) @@ -91,15 +91,15 @@ func TestVerifyTokens(t *testing.T) { } func TestVerifyIDToken(t *testing.T) { - verifier := &idTokenVerifier{ - issuer: tu.ValidIssuer, - maxAgeIAT: 2 * time.Minute, - offset: time.Second, - supportedSignAlgs: []string{string(tu.SignatureAlgorithm)}, - keySet: tu.KeySet{}, - maxAge: 2 * time.Minute, - acr: tu.ACRVerify, - nonce: func(context.Context) string { return tu.ValidNonce }, + verifier := &IDTokenVerifier{ + Issuer: tu.ValidIssuer, + MaxAgeIAT: 2 * time.Minute, + Offset: time.Second, + SupportedSignAlgs: []string{string(tu.SignatureAlgorithm)}, + KeySet: tu.KeySet{}, + MaxAge: 2 * time.Minute, + ACR: tu.ACRVerify, + Nonce: func(context.Context) string { return tu.ValidNonce }, } tests := []struct { @@ -219,7 +219,7 @@ func TestVerifyIDToken(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { token, want := tt.tokenClaims() - verifier.clientID = tt.clientID + verifier.ClientID = tt.clientID got, err := VerifyIDToken[*oidc.IDTokenClaims](context.Background(), token, verifier) if tt.wantErr { assert.Error(t, err) @@ -300,7 +300,7 @@ func TestNewIDTokenVerifier(t *testing.T) { tests := []struct { name string args args - want IDTokenVerifier + want *IDTokenVerifier }{ { name: "nil nonce", // otherwise assert.Equal will fail on the function @@ -317,16 +317,16 @@ func TestNewIDTokenVerifier(t *testing.T) { WithSupportedSigningAlgorithms("ABC", "DEF"), }, }, - want: &idTokenVerifier{ - issuer: tu.ValidIssuer, - offset: time.Minute, - maxAgeIAT: time.Hour, - clientID: tu.ValidClientID, - keySet: tu.KeySet{}, - nonce: nil, - acr: nil, - maxAge: 2 * time.Hour, - supportedSignAlgs: []string{"ABC", "DEF"}, + want: &IDTokenVerifier{ + Issuer: tu.ValidIssuer, + Offset: time.Minute, + MaxAgeIAT: time.Hour, + ClientID: tu.ValidClientID, + KeySet: tu.KeySet{}, + Nonce: nil, + ACR: nil, + MaxAge: 2 * time.Hour, + SupportedSignAlgs: []string{"ABC", "DEF"}, }, }, } diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index e63e0e5..6b6945a 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -192,7 +192,7 @@ func (j *JWTTokenRequest) GetExpiration() time.Time { // GetIssuedAt implements the Claims interface func (j *JWTTokenRequest) GetIssuedAt() time.Time { - return j.ExpiresAt.AsTime() + return j.IssuedAt.AsTime() } // GetNonce implements the Claims interface diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index cb513a0..167f8b7 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -173,10 +173,16 @@ func NewEncoder() *schema.Encoder { type Time int64 func (ts Time) AsTime() time.Time { + if ts == 0 { + return time.Time{} + } return time.Unix(int64(ts), 0) } func FromTime(tt time.Time) Time { + if tt.IsZero() { + return 0 + } return Time(tt.Unix()) } diff --git a/pkg/oidc/types_test.go b/pkg/oidc/types_test.go index 2721e0b..64f07f1 100644 --- a/pkg/oidc/types_test.go +++ b/pkg/oidc/types_test.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" "testing" + "time" "github.com/gorilla/schema" "github.com/stretchr/testify/assert" @@ -467,6 +468,56 @@ func TestNewEncoder(t *testing.T) { assert.Equal(t, a, b) } +func TestTime_AsTime(t *testing.T) { + tests := []struct { + name string + ts Time + want time.Time + }{ + { + name: "unset", + ts: 0, + want: time.Time{}, + }, + { + name: "set", + ts: 1, + want: time.Unix(1, 0), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.ts.AsTime() + assert.Equal(t, tt.want, got) + }) + } +} + +func TestTime_FromTime(t *testing.T) { + tests := []struct { + name string + tt time.Time + want Time + }{ + { + name: "zero", + tt: time.Time{}, + want: 0, + }, + { + name: "set", + tt: time.Unix(1, 0), + want: 1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := FromTime(tt.tt) + assert.Equal(t, tt.want, got) + }) + } +} + func TestTime_UnmarshalJSON(t *testing.T) { type dst struct { UpdatedAt Time `json:"updated_at"` diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go index ad82617..2d4e7a6 100644 --- a/pkg/oidc/verifier.go +++ b/pkg/oidc/verifier.go @@ -61,10 +61,19 @@ var ( ErrAtHash = errors.New("at_hash does not correspond to access token") ) -type Verifier interface { - Issuer() string - MaxAgeIAT() time.Duration - Offset() time.Duration +// Verifier caries configuration for the various token verification +// functions. Use package specific constructor functions to know +// which values need to be set. +type Verifier struct { + Issuer string + MaxAgeIAT time.Duration + Offset time.Duration + ClientID string + SupportedSignAlgs []string + MaxAge time.Duration + ACR ACRVerifier + KeySet KeySet + Nonce func(ctx context.Context) string } // ACRVerifier specifies the function to be used by the `DefaultVerifier` for validating the acr claim @@ -121,6 +130,11 @@ func CheckAudience(claims Claims, clientID string) error { return nil } +// CheckAuthorizedParty checks azp (authorized party) claim requirements. +// +// If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present. +// If an azp Claim is present, the Client SHOULD verify that its client_id is the Claim Value. +// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation func CheckAuthorizedParty(claims Claims, clientID string) error { if len(claims.GetAudience()) > 1 { if claims.GetAuthorizedParty() == "" { @@ -167,26 +181,26 @@ func CheckSignature(ctx context.Context, token string, payload []byte, claims Cl } func CheckExpiration(claims Claims, offset time.Duration) error { - expiration := claims.GetExpiration().Round(time.Second) - if !time.Now().UTC().Add(offset).Before(expiration) { + expiration := claims.GetExpiration() + if !time.Now().Add(offset).Before(expiration) { return ErrExpired } return nil } func CheckIssuedAt(claims Claims, maxAgeIAT, offset time.Duration) error { - issuedAt := claims.GetIssuedAt().Round(time.Second) + issuedAt := claims.GetIssuedAt() if issuedAt.IsZero() { return ErrIatMissing } - nowWithOffset := time.Now().UTC().Add(offset).Round(time.Second) + nowWithOffset := time.Now().Add(offset).Round(time.Second) if issuedAt.After(nowWithOffset) { return fmt.Errorf("%w: (iat: %v, now with offset: %v)", ErrIatInFuture, issuedAt, nowWithOffset) } if maxAgeIAT == 0 { return nil } - maxAge := time.Now().UTC().Add(-maxAgeIAT).Round(time.Second) + maxAge := time.Now().Add(-maxAgeIAT).Round(time.Second) if issuedAt.Before(maxAge) { return fmt.Errorf("%w: must not be older than %v, but was %v (%v to old)", ErrIatToOld, maxAge, issuedAt, maxAge.Sub(issuedAt)) } @@ -216,8 +230,8 @@ func CheckAuthTime(claims Claims, maxAge time.Duration) error { if claims.GetAuthTime().IsZero() { return ErrAuthTimeNotPresent } - authTime := claims.GetAuthTime().Round(time.Second) - maxAuthTime := time.Now().UTC().Add(-maxAge).Round(time.Second) + authTime := claims.GetAuthTime() + maxAuthTime := time.Now().Add(-maxAge).Round(time.Second) if authTime.Before(maxAuthTime) { return fmt.Errorf("%w: must not be older than %v, but was %v (%v to old)", ErrAuthTimeToOld, maxAge, authTime, maxAuthTime.Sub(authTime)) } diff --git a/pkg/oidc/verifier_parse_test.go b/pkg/oidc/verifier_parse_test.go new file mode 100644 index 0000000..105650f --- /dev/null +++ b/pkg/oidc/verifier_parse_test.go @@ -0,0 +1,128 @@ +package oidc_test + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + tu "github.com/zitadel/oidc/v3/internal/testutil" + "github.com/zitadel/oidc/v3/pkg/oidc" +) + +func TestParseToken(t *testing.T) { + token, wantClaims := tu.ValidIDToken() + wantClaims.SignatureAlg = "" // unset, because is not part of the JSON payload + + wantPayload, err := json.Marshal(wantClaims) + require.NoError(t, err) + + tests := []struct { + name string + tokenString string + wantErr bool + }{ + { + name: "split error", + tokenString: "nope", + wantErr: true, + }, + { + name: "base64 error", + tokenString: "foo.~.bar", + wantErr: true, + }, + { + name: "success", + tokenString: token, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotClaims := new(oidc.IDTokenClaims) + gotPayload, err := oidc.ParseToken(tt.tokenString, gotClaims) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, wantClaims, gotClaims) + assert.JSONEq(t, string(wantPayload), string(gotPayload)) + }) + } +} + +func TestCheckSignature(t *testing.T) { + errCtx, cancel := context.WithCancel(context.Background()) + cancel() + + token, _ := tu.ValidIDToken() + payload, err := oidc.ParseToken(token, &oidc.IDTokenClaims{}) + require.NoError(t, err) + + type args struct { + ctx context.Context + token string + payload []byte + supportedSigAlgs []string + } + tests := []struct { + name string + args args + wantErr error + }{ + { + name: "parse error", + args: args{ + ctx: context.Background(), + token: "~", + payload: payload, + }, + wantErr: oidc.ErrParse, + }, + { + name: "default sigAlg", + args: args{ + ctx: context.Background(), + token: token, + payload: payload, + }, + }, + { + name: "unsupported sigAlg", + args: args{ + ctx: context.Background(), + token: token, + payload: payload, + supportedSigAlgs: []string{"foo", "bar"}, + }, + wantErr: oidc.ErrSignatureUnsupportedAlg, + }, + { + name: "verify error", + args: args{ + ctx: errCtx, + token: token, + payload: payload, + }, + wantErr: oidc.ErrSignatureInvalid, + }, + { + name: "inequal payloads", + args: args{ + ctx: context.Background(), + token: token, + payload: []byte{0, 1, 2}, + }, + wantErr: oidc.ErrSignatureInvalidPayload, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + claims := new(oidc.TokenClaims) + err := oidc.CheckSignature(tt.args.ctx, tt.args.token, tt.args.payload, claims, tt.args.supportedSigAlgs, tu.KeySet{}) + assert.ErrorIs(t, err, tt.wantErr) + }) + } +} diff --git a/pkg/oidc/verifier_test.go b/pkg/oidc/verifier_test.go new file mode 100644 index 0000000..93e7157 --- /dev/null +++ b/pkg/oidc/verifier_test.go @@ -0,0 +1,374 @@ +package oidc + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDecryptToken(t *testing.T) { + const tokenString = "ABC" + got, err := DecryptToken(tokenString) + require.NoError(t, err) + assert.Equal(t, tokenString, got) +} + +func TestDefaultACRVerifier(t *testing.T) { + acrVerfier := DefaultACRVerifier([]string{"foo", "bar"}) + + tests := []struct { + name string + acr string + wantErr string + }{ + { + name: "ok", + acr: "bar", + }, + { + name: "error", + acr: "hello", + wantErr: "expected one of: [foo bar], got: \"hello\"", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := acrVerfier(tt.acr) + if tt.wantErr != "" { + assert.EqualError(t, err, tt.wantErr) + return + } + require.NoError(t, err) + }) + } +} + +func TestCheckSubject(t *testing.T) { + tests := []struct { + name string + claims Claims + wantErr error + }{ + { + name: "missing", + claims: &TokenClaims{}, + wantErr: ErrSubjectMissing, + }, + { + name: "ok", + claims: &TokenClaims{ + Subject: "foo", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := CheckSubject(tt.claims) + assert.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestCheckIssuer(t *testing.T) { + const issuer = "foo.bar" + tests := []struct { + name string + claims Claims + wantErr error + }{ + { + name: "missing", + claims: &TokenClaims{}, + wantErr: ErrIssuerInvalid, + }, + { + name: "wrong", + claims: &TokenClaims{ + Issuer: "wrong", + }, + wantErr: ErrIssuerInvalid, + }, + { + name: "ok", + claims: &TokenClaims{ + Issuer: issuer, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := CheckIssuer(tt.claims, issuer) + assert.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestCheckAudience(t *testing.T) { + const clientID = "foo.bar" + tests := []struct { + name string + claims Claims + wantErr error + }{ + { + name: "missing", + claims: &TokenClaims{}, + wantErr: ErrAudience, + }, + { + name: "wrong", + claims: &TokenClaims{ + Audience: []string{"wrong"}, + }, + wantErr: ErrAudience, + }, + { + name: "ok", + claims: &TokenClaims{ + Audience: []string{clientID}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := CheckAudience(tt.claims, clientID) + assert.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestCheckAuthorizedParty(t *testing.T) { + const clientID = "foo.bar" + tests := []struct { + name string + claims Claims + wantErr error + }{ + { + name: "single audience, no azp", + claims: &TokenClaims{ + Audience: []string{clientID}, + }, + }, + { + name: "multiple audience, no azp", + claims: &TokenClaims{ + Audience: []string{clientID, "other"}, + }, + wantErr: ErrAzpMissing, + }, + { + name: "single audience, with azp", + claims: &TokenClaims{ + Audience: []string{clientID}, + AuthorizedParty: clientID, + }, + }, + { + name: "multiple audience, with azp", + claims: &TokenClaims{ + Audience: []string{clientID, "other"}, + AuthorizedParty: clientID, + }, + }, + { + name: "wrong azp", + claims: &TokenClaims{ + AuthorizedParty: "wrong", + }, + wantErr: ErrAzpInvalid, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := CheckAuthorizedParty(tt.claims, clientID) + assert.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestCheckExpiration(t *testing.T) { + const offset = time.Minute + tests := []struct { + name string + claims Claims + wantErr error + }{ + { + name: "missing", + claims: &TokenClaims{}, + wantErr: ErrExpired, + }, + { + name: "expired", + claims: &TokenClaims{ + Expiration: FromTime(time.Now().Add(-2 * offset)), + }, + wantErr: ErrExpired, + }, + { + name: "valid", + claims: &TokenClaims{ + Expiration: FromTime(time.Now().Add(2 * offset)), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := CheckExpiration(tt.claims, offset) + assert.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestCheckIssuedAt(t *testing.T) { + const offset = time.Minute + tests := []struct { + name string + maxAgeIAT time.Duration + claims Claims + wantErr error + }{ + { + name: "missing", + claims: &TokenClaims{}, + wantErr: ErrIatMissing, + }, + { + name: "future", + claims: &TokenClaims{ + IssuedAt: FromTime(time.Now().Add(time.Hour)), + }, + wantErr: ErrIatInFuture, + }, + { + name: "no max", + claims: &TokenClaims{ + IssuedAt: FromTime(time.Now()), + }, + }, + { + name: "past max", + maxAgeIAT: time.Minute, + claims: &TokenClaims{ + IssuedAt: FromTime(time.Now().Add(-time.Hour)), + }, + wantErr: ErrIatToOld, + }, + { + name: "within max", + maxAgeIAT: time.Hour, + claims: &TokenClaims{ + IssuedAt: FromTime(time.Now()), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := CheckIssuedAt(tt.claims, tt.maxAgeIAT, offset) + assert.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestCheckNonce(t *testing.T) { + const nonce = "123" + tests := []struct { + name string + claims Claims + wantErr error + }{ + { + name: "missing", + claims: &TokenClaims{}, + wantErr: ErrNonceInvalid, + }, + { + name: "wrong", + claims: &TokenClaims{ + Nonce: "wrong", + }, + wantErr: ErrNonceInvalid, + }, + { + name: "ok", + claims: &TokenClaims{ + Nonce: nonce, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := CheckNonce(tt.claims, nonce) + assert.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestCheckAuthorizationContextClassReference(t *testing.T) { + tests := []struct { + name string + acr ACRVerifier + wantErr error + }{ + { + name: "error", + acr: func(s string) error { return errors.New("oops") }, + wantErr: ErrAcrInvalid, + }, + { + name: "ok", + acr: func(s string) error { return nil }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := CheckAuthorizationContextClassReference(&IDTokenClaims{}, tt.acr) + assert.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestCheckAuthTime(t *testing.T) { + tests := []struct { + name string + claims Claims + maxAge time.Duration + wantErr error + }{ + { + name: "no max age", + claims: &TokenClaims{}, + }, + { + name: "missing", + claims: &TokenClaims{}, + maxAge: time.Minute, + wantErr: ErrAuthTimeNotPresent, + }, + { + name: "expired", + maxAge: time.Minute, + claims: &TokenClaims{ + AuthTime: FromTime(time.Now().Add(-time.Hour)), + }, + wantErr: ErrAuthTimeToOld, + }, + { + name: "ok", + maxAge: time.Minute, + claims: &TokenClaims{ + AuthTime: NowTime(), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := CheckAuthTime(tt.claims, tt.maxAge) + assert.ErrorIs(t, err, tt.wantErr) + }) + } +} diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 1d1add5..b516909 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -38,7 +38,7 @@ type Authorizer interface { Storage() Storage Decoder() httphelper.Decoder Encoder() httphelper.Encoder - IDTokenHintVerifier(context.Context) IDTokenHintVerifier + IDTokenHintVerifier(context.Context) *IDTokenHintVerifier Crypto() Crypto RequestObjectSupported() bool } @@ -47,7 +47,7 @@ type Authorizer interface { // implementing its own validation mechanism for the auth request type AuthorizeValidator interface { Authorizer - ValidateAuthRequest(context.Context, *oidc.AuthRequest, Storage, IDTokenHintVerifier) (string, error) + ValidateAuthRequest(context.Context, *oidc.AuthRequest, Storage, *IDTokenHintVerifier) (string, error) } func authorizeHandler(authorizer Authorizer) func(http.ResponseWriter, *http.Request) { @@ -204,7 +204,7 @@ func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oi } // ValidateAuthRequest validates the authorize parameters and returns the userID of the id_token_hint if passed -func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier IDTokenHintVerifier) (sub string, err error) { +func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier *IDTokenHintVerifier) (sub string, err error) { authReq.MaxAge, err = ValidateAuthReqPrompt(authReq.Prompt, authReq.MaxAge) if err != nil { return "", err @@ -384,7 +384,7 @@ func ValidateAuthReqResponseType(client Client, responseType oidc.ResponseType) // ValidateAuthReqIDTokenHint validates the id_token_hint (if passed as parameter in the request) // and returns the `sub` claim -func ValidateAuthReqIDTokenHint(ctx context.Context, idTokenHint string, verifier IDTokenHintVerifier) (string, error) { +func ValidateAuthReqIDTokenHint(ctx context.Context, idTokenHint string, verifier *IDTokenHintVerifier) (string, error) { if idTokenHint == "" { return "", nil } diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index 65c65da..3179e25 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -12,6 +12,7 @@ import ( "github.com/gorilla/schema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + tu "github.com/zitadel/oidc/v3/internal/testutil" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" @@ -146,7 +147,7 @@ func TestValidateAuthRequest(t *testing.T) { type args struct { authRequest *oidc.AuthRequest storage op.Storage - verifier op.IDTokenHintVerifier + verifier *op.IDTokenHintVerifier } tests := []struct { name string @@ -1003,3 +1004,34 @@ func Test_parseAuthorizeCallbackRequest(t *testing.T) { }) } } + +func TestValidateAuthReqIDTokenHint(t *testing.T) { + token, _ := tu.ValidIDToken() + tests := []struct { + name string + idTokenHint string + want string + wantErr error + }{ + { + name: "empty", + }, + { + name: "verify err", + idTokenHint: "foo", + wantErr: oidc.ErrLoginRequired(), + }, + { + name: "ok", + idTokenHint: token, + want: tu.ValidSubject, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := op.ValidateAuthReqIDTokenHint(context.Background(), tt.idTokenHint, op.NewIDTokenHintVerifier(tu.ValidIssuer, tu.KeySet{})) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/op/client.go b/pkg/op/client.go index 175caec..754636c 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -81,7 +81,7 @@ var ( ) type ClientJWTProfile interface { - JWTProfileVerifier(context.Context) JWTProfileVerifier + JWTProfileVerifier(context.Context) *JWTProfileVerifier } func ClientJWTAuth(ctx context.Context, ca oidc.ClientAssertionParams, verifier ClientJWTProfile) (clientID string, err error) { diff --git a/pkg/op/client_test.go b/pkg/op/client_test.go index 2e40d9a..bb17192 100644 --- a/pkg/op/client_test.go +++ b/pkg/op/client_test.go @@ -22,7 +22,7 @@ import ( type testClientJWTProfile struct{} -func (testClientJWTProfile) JWTProfileVerifier(context.Context) op.JWTProfileVerifier { return nil } +func (testClientJWTProfile) JWTProfileVerifier(context.Context) *op.JWTProfileVerifier { return nil } func TestClientJWTAuth(t *testing.T) { type args struct { diff --git a/pkg/op/mock/authorizer.mock.go b/pkg/op/mock/authorizer.mock.go index 931b896..a0c67e3 100644 --- a/pkg/op/mock/authorizer.mock.go +++ b/pkg/op/mock/authorizer.mock.go @@ -79,10 +79,10 @@ func (mr *MockAuthorizerMockRecorder) Encoder() *gomock.Call { } // IDTokenHintVerifier mocks base method. -func (m *MockAuthorizer) IDTokenHintVerifier(arg0 context.Context) op.IDTokenHintVerifier { +func (m *MockAuthorizer) IDTokenHintVerifier(arg0 context.Context) *op.IDTokenHintVerifier { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "IDTokenHintVerifier", arg0) - ret0, _ := ret[0].(op.IDTokenHintVerifier) + ret0, _ := ret[0].(*op.IDTokenHintVerifier) return ret0 } diff --git a/pkg/op/mock/authorizer.mock.impl.go b/pkg/op/mock/authorizer.mock.impl.go index 6a5bdfd..409683a 100644 --- a/pkg/op/mock/authorizer.mock.impl.go +++ b/pkg/op/mock/authorizer.mock.impl.go @@ -49,7 +49,7 @@ func ExpectEncoder(a op.Authorizer) { func ExpectVerifier(a op.Authorizer, t *testing.T) { mockA := a.(*MockAuthorizer) mockA.EXPECT().IDTokenHintVerifier(gomock.Any()).DoAndReturn( - func() op.IDTokenHintVerifier { + func() *op.IDTokenHintVerifier { return op.NewIDTokenHintVerifier("", nil) }) } diff --git a/pkg/op/op.go b/pkg/op/op.go index 27c1410..9ed5662 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -73,8 +73,8 @@ type OpenIDProvider interface { Storage() Storage Decoder() httphelper.Decoder Encoder() httphelper.Encoder - IDTokenHintVerifier(context.Context) IDTokenHintVerifier - AccessTokenVerifier(context.Context) AccessTokenVerifier + IDTokenHintVerifier(context.Context) *IDTokenHintVerifier + AccessTokenVerifier(context.Context) *AccessTokenVerifier Crypto() Crypto DefaultLogoutRedirectURI() string Probes() []ProbesFn @@ -342,15 +342,15 @@ func (o *Provider) Encoder() httphelper.Encoder { return o.encoder } -func (o *Provider) IDTokenHintVerifier(ctx context.Context) IDTokenHintVerifier { +func (o *Provider) IDTokenHintVerifier(ctx context.Context) *IDTokenHintVerifier { return NewIDTokenHintVerifier(IssuerFromContext(ctx), o.openIDKeySet(), o.idTokenHintVerifierOpts...) } -func (o *Provider) JWTProfileVerifier(ctx context.Context) JWTProfileVerifier { +func (o *Provider) JWTProfileVerifier(ctx context.Context) *JWTProfileVerifier { return NewJWTProfileVerifier(o.Storage(), IssuerFromContext(ctx), 1*time.Hour, time.Second) } -func (o *Provider) AccessTokenVerifier(ctx context.Context) AccessTokenVerifier { +func (o *Provider) AccessTokenVerifier(ctx context.Context) *AccessTokenVerifier { return NewAccessTokenVerifier(IssuerFromContext(ctx), o.openIDKeySet(), o.accessTokenVerifierOpts...) } diff --git a/pkg/op/session.go b/pkg/op/session.go index fbce125..fd914d1 100644 --- a/pkg/op/session.go +++ b/pkg/op/session.go @@ -13,7 +13,7 @@ import ( type SessionEnder interface { Decoder() httphelper.Decoder Storage() Storage - IDTokenHintVerifier(context.Context) IDTokenHintVerifier + IDTokenHintVerifier(context.Context) *IDTokenHintVerifier DefaultLogoutRedirectURI() string } diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go index 28df217..21b79c3 100644 --- a/pkg/op/token_intospection.go +++ b/pkg/op/token_intospection.go @@ -13,7 +13,7 @@ type Introspector interface { Decoder() httphelper.Decoder Crypto() Crypto Storage() Storage - AccessTokenVerifier(context.Context) AccessTokenVerifier + AccessTokenVerifier(context.Context) *AccessTokenVerifier } type IntrospectorJWTProfile interface { diff --git a/pkg/op/token_jwt_profile.go b/pkg/op/token_jwt_profile.go index 4563e16..4cd7b1e 100644 --- a/pkg/op/token_jwt_profile.go +++ b/pkg/op/token_jwt_profile.go @@ -11,7 +11,7 @@ import ( type JWTAuthorizationGrantExchanger interface { Exchanger - JWTProfileVerifier(context.Context) JWTProfileVerifier + JWTProfileVerifier(context.Context) *JWTProfileVerifier } // JWTProfile handles the OAuth 2.0 JWT Profile Authorization Grant https://tools.ietf.org/html/rfc7523#section-2.1 diff --git a/pkg/op/token_request.go b/pkg/op/token_request.go index 058a202..c06a51b 100644 --- a/pkg/op/token_request.go +++ b/pkg/op/token_request.go @@ -20,8 +20,8 @@ type Exchanger interface { GrantTypeJWTAuthorizationSupported() bool GrantTypeClientCredentialsSupported() bool GrantTypeDeviceCodeSupported() bool - AccessTokenVerifier(context.Context) AccessTokenVerifier - IDTokenHintVerifier(context.Context) IDTokenHintVerifier + AccessTokenVerifier(context.Context) *AccessTokenVerifier + IDTokenHintVerifier(context.Context) *IDTokenHintVerifier } func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/op/token_revocation.go b/pkg/op/token_revocation.go index 34f8746..fd1ee93 100644 --- a/pkg/op/token_revocation.go +++ b/pkg/op/token_revocation.go @@ -15,14 +15,14 @@ type Revoker interface { Decoder() httphelper.Decoder Crypto() Crypto Storage() Storage - AccessTokenVerifier(context.Context) AccessTokenVerifier + AccessTokenVerifier(context.Context) *AccessTokenVerifier AuthMethodPrivateKeyJWTSupported() bool AuthMethodPostSupported() bool } type RevokerJWTProfile interface { Revoker - JWTProfileVerifier(context.Context) JWTProfileVerifier + JWTProfileVerifier(context.Context) *JWTProfileVerifier } func revocationHandler(revoker Revoker) func(http.ResponseWriter, *http.Request) { diff --git a/pkg/op/userinfo.go b/pkg/op/userinfo.go index 52a2aa2..86205b5 100644 --- a/pkg/op/userinfo.go +++ b/pkg/op/userinfo.go @@ -14,7 +14,7 @@ type UserinfoProvider interface { Decoder() httphelper.Decoder Crypto() Crypto Storage() Storage - AccessTokenVerifier(context.Context) AccessTokenVerifier + AccessTokenVerifier(context.Context) *AccessTokenVerifier } func userinfoHandler(userinfoProvider UserinfoProvider) func(http.ResponseWriter, *http.Request) { diff --git a/pkg/op/verifier_access_token.go b/pkg/op/verifier_access_token.go index 7527ea6..120bfa7 100644 --- a/pkg/op/verifier_access_token.go +++ b/pkg/op/verifier_access_token.go @@ -2,62 +2,25 @@ package op import ( "context" - "time" "github.com/zitadel/oidc/v3/pkg/oidc" ) -type AccessTokenVerifier interface { - oidc.Verifier - SupportedSignAlgs() []string - KeySet() oidc.KeySet -} +type AccessTokenVerifier oidc.Verifier -type accessTokenVerifier struct { - issuer string - maxAgeIAT time.Duration - offset time.Duration - supportedSignAlgs []string - keySet oidc.KeySet -} - -// Issuer implements oidc.Verifier interface -func (i *accessTokenVerifier) Issuer() string { - return i.issuer -} - -// MaxAgeIAT implements oidc.Verifier interface -func (i *accessTokenVerifier) MaxAgeIAT() time.Duration { - return i.maxAgeIAT -} - -// Offset implements oidc.Verifier interface -func (i *accessTokenVerifier) Offset() time.Duration { - return i.offset -} - -// SupportedSignAlgs implements AccessTokenVerifier interface -func (i *accessTokenVerifier) SupportedSignAlgs() []string { - return i.supportedSignAlgs -} - -// KeySet implements AccessTokenVerifier interface -func (i *accessTokenVerifier) KeySet() oidc.KeySet { - return i.keySet -} - -type AccessTokenVerifierOpt func(*accessTokenVerifier) +type AccessTokenVerifierOpt func(*AccessTokenVerifier) func WithSupportedAccessTokenSigningAlgorithms(algs ...string) AccessTokenVerifierOpt { - return func(verifier *accessTokenVerifier) { - verifier.supportedSignAlgs = algs + return func(verifier *AccessTokenVerifier) { + verifier.SupportedSignAlgs = algs } } -func NewAccessTokenVerifier(issuer string, keySet oidc.KeySet, opts ...AccessTokenVerifierOpt) AccessTokenVerifier { - verifier := &accessTokenVerifier{ - issuer: issuer, - keySet: keySet, +// NewAccessTokenVerifier returns a AccessTokenVerifier suitable for access token verification. +func NewAccessTokenVerifier(issuer string, keySet oidc.KeySet, opts ...AccessTokenVerifierOpt) *AccessTokenVerifier { + verifier := &AccessTokenVerifier{ + Issuer: issuer, + KeySet: keySet, } for _, opt := range opts { opt(verifier) @@ -66,7 +29,7 @@ func NewAccessTokenVerifier(issuer string, keySet oidc.KeySet, opts ...AccessTok } // VerifyAccessToken validates the access token (issuer, signature and expiration). -func VerifyAccessToken[C oidc.Claims](ctx context.Context, token string, v AccessTokenVerifier) (claims C, err error) { +func VerifyAccessToken[C oidc.Claims](ctx context.Context, token string, v *AccessTokenVerifier) (claims C, err error) { var nilClaims C decrypted, err := oidc.DecryptToken(token) @@ -78,15 +41,15 @@ func VerifyAccessToken[C oidc.Claims](ctx context.Context, token string, v Acces return nilClaims, err } - if err := oidc.CheckIssuer(claims, v.Issuer()); err != nil { + if err := oidc.CheckIssuer(claims, v.Issuer); err != nil { return nilClaims, err } - if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil { + if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs, v.KeySet); err != nil { return nilClaims, err } - if err = oidc.CheckExpiration(claims, v.Offset()); err != nil { + if err = oidc.CheckExpiration(claims, v.Offset); err != nil { return nilClaims, err } diff --git a/pkg/op/verifier_access_token_test.go b/pkg/op/verifier_access_token_test.go index a1972f1..66e32ce 100644 --- a/pkg/op/verifier_access_token_test.go +++ b/pkg/op/verifier_access_token_test.go @@ -20,7 +20,7 @@ func TestNewAccessTokenVerifier(t *testing.T) { tests := []struct { name string args args - want AccessTokenVerifier + want *AccessTokenVerifier }{ { name: "simple", @@ -28,9 +28,9 @@ func TestNewAccessTokenVerifier(t *testing.T) { issuer: tu.ValidIssuer, keySet: tu.KeySet{}, }, - want: &accessTokenVerifier{ - issuer: tu.ValidIssuer, - keySet: tu.KeySet{}, + want: &AccessTokenVerifier{ + Issuer: tu.ValidIssuer, + KeySet: tu.KeySet{}, }, }, { @@ -42,10 +42,10 @@ func TestNewAccessTokenVerifier(t *testing.T) { WithSupportedAccessTokenSigningAlgorithms("ABC", "DEF"), }, }, - want: &accessTokenVerifier{ - issuer: tu.ValidIssuer, - keySet: tu.KeySet{}, - supportedSignAlgs: []string{"ABC", "DEF"}, + want: &AccessTokenVerifier{ + Issuer: tu.ValidIssuer, + KeySet: tu.KeySet{}, + SupportedSignAlgs: []string{"ABC", "DEF"}, }, }, } @@ -58,12 +58,12 @@ func TestNewAccessTokenVerifier(t *testing.T) { } func TestVerifyAccessToken(t *testing.T) { - verifier := &accessTokenVerifier{ - issuer: tu.ValidIssuer, - maxAgeIAT: 2 * time.Minute, - offset: time.Second, - supportedSignAlgs: []string{string(tu.SignatureAlgorithm)}, - keySet: tu.KeySet{}, + verifier := &AccessTokenVerifier{ + Issuer: tu.ValidIssuer, + MaxAgeIAT: 2 * time.Minute, + Offset: time.Second, + SupportedSignAlgs: []string{string(tu.SignatureAlgorithm)}, + KeySet: tu.KeySet{}, } tests := []struct { diff --git a/pkg/op/verifier_id_token_hint.go b/pkg/op/verifier_id_token_hint.go index 50c3ff6..6143252 100644 --- a/pkg/op/verifier_id_token_hint.go +++ b/pkg/op/verifier_id_token_hint.go @@ -2,69 +2,24 @@ package op import ( "context" - "time" "github.com/zitadel/oidc/v3/pkg/oidc" ) -type IDTokenHintVerifier interface { - oidc.Verifier - SupportedSignAlgs() []string - KeySet() oidc.KeySet - ACR() oidc.ACRVerifier - MaxAge() time.Duration -} +type IDTokenHintVerifier oidc.Verifier -type idTokenHintVerifier struct { - issuer string - maxAgeIAT time.Duration - offset time.Duration - supportedSignAlgs []string - maxAge time.Duration - acr oidc.ACRVerifier - keySet oidc.KeySet -} - -func (i *idTokenHintVerifier) Issuer() string { - return i.issuer -} - -func (i *idTokenHintVerifier) MaxAgeIAT() time.Duration { - return i.maxAgeIAT -} - -func (i *idTokenHintVerifier) Offset() time.Duration { - return i.offset -} - -func (i *idTokenHintVerifier) SupportedSignAlgs() []string { - return i.supportedSignAlgs -} - -func (i *idTokenHintVerifier) KeySet() oidc.KeySet { - return i.keySet -} - -func (i *idTokenHintVerifier) ACR() oidc.ACRVerifier { - return i.acr -} - -func (i *idTokenHintVerifier) MaxAge() time.Duration { - return i.maxAge -} - -type IDTokenHintVerifierOpt func(*idTokenHintVerifier) +type IDTokenHintVerifierOpt func(*IDTokenHintVerifier) func WithSupportedIDTokenHintSigningAlgorithms(algs ...string) IDTokenHintVerifierOpt { - return func(verifier *idTokenHintVerifier) { - verifier.supportedSignAlgs = algs + return func(verifier *IDTokenHintVerifier) { + verifier.SupportedSignAlgs = algs } } -func NewIDTokenHintVerifier(issuer string, keySet oidc.KeySet, opts ...IDTokenHintVerifierOpt) IDTokenHintVerifier { - verifier := &idTokenHintVerifier{ - issuer: issuer, - keySet: keySet, +func NewIDTokenHintVerifier(issuer string, keySet oidc.KeySet, opts ...IDTokenHintVerifierOpt) *IDTokenHintVerifier { + verifier := &IDTokenHintVerifier{ + Issuer: issuer, + KeySet: keySet, } for _, opt := range opts { opt(verifier) @@ -74,7 +29,7 @@ func NewIDTokenHintVerifier(issuer string, keySet oidc.KeySet, opts ...IDTokenHi // VerifyIDTokenHint validates the id token according to // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation -func VerifyIDTokenHint[C oidc.Claims](ctx context.Context, token string, v IDTokenHintVerifier) (claims C, err error) { +func VerifyIDTokenHint[C oidc.Claims](ctx context.Context, token string, v *IDTokenHintVerifier) (claims C, err error) { var nilClaims C decrypted, err := oidc.DecryptToken(token) @@ -86,27 +41,27 @@ func VerifyIDTokenHint[C oidc.Claims](ctx context.Context, token string, v IDTok return nilClaims, err } - if err := oidc.CheckIssuer(claims, v.Issuer()); err != nil { + if err := oidc.CheckIssuer(claims, v.Issuer); err != nil { return nilClaims, err } - if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil { + if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs, v.KeySet); err != nil { return nilClaims, err } - if err = oidc.CheckExpiration(claims, v.Offset()); err != nil { + if err = oidc.CheckExpiration(claims, v.Offset); err != nil { return nilClaims, err } - if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT(), v.Offset()); err != nil { + if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT, v.Offset); err != nil { return nilClaims, err } - if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR()); err != nil { + if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR); err != nil { return nilClaims, err } - if err = oidc.CheckAuthTime(claims, v.MaxAge()); err != nil { + if err = oidc.CheckAuthTime(claims, v.MaxAge); err != nil { return nilClaims, err } return claims, nil diff --git a/pkg/op/verifier_id_token_hint_test.go b/pkg/op/verifier_id_token_hint_test.go index 9f4c6c1..e514a76 100644 --- a/pkg/op/verifier_id_token_hint_test.go +++ b/pkg/op/verifier_id_token_hint_test.go @@ -20,7 +20,7 @@ func TestNewIDTokenHintVerifier(t *testing.T) { tests := []struct { name string args args - want IDTokenHintVerifier + want *IDTokenHintVerifier }{ { name: "simple", @@ -28,9 +28,9 @@ func TestNewIDTokenHintVerifier(t *testing.T) { issuer: tu.ValidIssuer, keySet: tu.KeySet{}, }, - want: &idTokenHintVerifier{ - issuer: tu.ValidIssuer, - keySet: tu.KeySet{}, + want: &IDTokenHintVerifier{ + Issuer: tu.ValidIssuer, + KeySet: tu.KeySet{}, }, }, { @@ -42,10 +42,10 @@ func TestNewIDTokenHintVerifier(t *testing.T) { WithSupportedIDTokenHintSigningAlgorithms("ABC", "DEF"), }, }, - want: &idTokenHintVerifier{ - issuer: tu.ValidIssuer, - keySet: tu.KeySet{}, - supportedSignAlgs: []string{"ABC", "DEF"}, + want: &IDTokenHintVerifier{ + Issuer: tu.ValidIssuer, + KeySet: tu.KeySet{}, + SupportedSignAlgs: []string{"ABC", "DEF"}, }, }, } @@ -58,14 +58,14 @@ func TestNewIDTokenHintVerifier(t *testing.T) { } func TestVerifyIDTokenHint(t *testing.T) { - verifier := &idTokenHintVerifier{ - issuer: tu.ValidIssuer, - maxAgeIAT: 2 * time.Minute, - offset: time.Second, - supportedSignAlgs: []string{string(tu.SignatureAlgorithm)}, - maxAge: 2 * time.Minute, - acr: tu.ACRVerify, - keySet: tu.KeySet{}, + verifier := &IDTokenHintVerifier{ + Issuer: tu.ValidIssuer, + MaxAgeIAT: 2 * time.Minute, + Offset: time.Second, + SupportedSignAlgs: []string{string(tu.SignatureAlgorithm)}, + MaxAge: 2 * time.Minute, + ACR: tu.ACRVerify, + KeySet: tu.KeySet{}, } tests := []struct { diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index b7dfec7..1daa15f 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -11,28 +11,25 @@ import ( "github.com/zitadel/oidc/v3/pkg/oidc" ) -type JWTProfileVerifier interface { +// JWTProfileVerfiier extends oidc.Verifier with +// a jwtProfileKeyStorage and a function to check +// the subject in a token. +type JWTProfileVerifier struct { oidc.Verifier - Storage() jwtProfileKeyStorage - CheckSubject(request *oidc.JWTTokenRequest) error -} - -type jwtProfileVerifier struct { - storage jwtProfileKeyStorage - subjectCheck func(request *oidc.JWTTokenRequest) error - issuer string - maxAgeIAT time.Duration - offset time.Duration + Storage JWTProfileKeyStorage + CheckSubject func(request *oidc.JWTTokenRequest) error } // NewJWTProfileVerifier creates a oidc.Verifier for JWT Profile assertions (authorization grant and client authentication) -func NewJWTProfileVerifier(storage jwtProfileKeyStorage, issuer string, maxAgeIAT, offset time.Duration, opts ...JWTProfileVerifierOption) JWTProfileVerifier { - j := &jwtProfileVerifier{ - storage: storage, - subjectCheck: SubjectIsIssuer, - issuer: issuer, - maxAgeIAT: maxAgeIAT, - offset: offset, +func NewJWTProfileVerifier(storage JWTProfileKeyStorage, issuer string, maxAgeIAT, offset time.Duration, opts ...JWTProfileVerifierOption) *JWTProfileVerifier { + j := &JWTProfileVerifier{ + Verifier: oidc.Verifier{ + Issuer: issuer, + MaxAgeIAT: maxAgeIAT, + Offset: offset, + }, + Storage: storage, + CheckSubject: SubjectIsIssuer, } for _, opt := range opts { @@ -42,53 +39,35 @@ func NewJWTProfileVerifier(storage jwtProfileKeyStorage, issuer string, maxAgeIA return j } -type JWTProfileVerifierOption func(*jwtProfileVerifier) +type JWTProfileVerifierOption func(*JWTProfileVerifier) +// SubjectCheck sets a custom function to check the subject. +// Defaults to SubjectIsIssuer() func SubjectCheck(check func(request *oidc.JWTTokenRequest) error) JWTProfileVerifierOption { - return func(verifier *jwtProfileVerifier) { - verifier.subjectCheck = check + return func(verifier *JWTProfileVerifier) { + verifier.CheckSubject = check } } -func (v *jwtProfileVerifier) Issuer() string { - return v.issuer -} - -func (v *jwtProfileVerifier) Storage() jwtProfileKeyStorage { - return v.storage -} - -func (v *jwtProfileVerifier) MaxAgeIAT() time.Duration { - return v.maxAgeIAT -} - -func (v *jwtProfileVerifier) Offset() time.Duration { - return v.offset -} - -func (v *jwtProfileVerifier) CheckSubject(request *oidc.JWTTokenRequest) error { - return v.subjectCheck(request) -} - // VerifyJWTAssertion verifies the assertion string from JWT Profile (authorization grant and client authentication) // // checks audience, exp, iat, signature and that issuer and sub are the same -func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerifier) (*oidc.JWTTokenRequest, error) { +func VerifyJWTAssertion(ctx context.Context, assertion string, v *JWTProfileVerifier) (*oidc.JWTTokenRequest, error) { request := new(oidc.JWTTokenRequest) payload, err := oidc.ParseToken(assertion, request) if err != nil { return nil, err } - if err = oidc.CheckAudience(request, v.Issuer()); err != nil { + if err = oidc.CheckAudience(request, v.Issuer); err != nil { return nil, err } - if err = oidc.CheckExpiration(request, v.Offset()); err != nil { + if err = oidc.CheckExpiration(request, v.Offset); err != nil { return nil, err } - if err = oidc.CheckIssuedAt(request, v.MaxAgeIAT(), v.Offset()); err != nil { + if err = oidc.CheckIssuedAt(request, v.MaxAgeIAT, v.Offset); err != nil { return nil, err } @@ -96,17 +75,18 @@ func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerif return nil, err } - keySet := &jwtProfileKeySet{storage: v.Storage(), clientID: request.Issuer} + keySet := &jwtProfileKeySet{storage: v.Storage, clientID: request.Issuer} if err = oidc.CheckSignature(ctx, assertion, payload, request, nil, keySet); err != nil { return nil, err } return request, nil } -type jwtProfileKeyStorage interface { +type JWTProfileKeyStorage interface { GetKeyByIDAndClientID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) } +// SubjectIsIssuer func SubjectIsIssuer(request *oidc.JWTTokenRequest) error { if request.Issuer != request.Subject { return errors.New("delegation not allowed, issuer and sub must be identical") @@ -115,7 +95,7 @@ func SubjectIsIssuer(request *oidc.JWTTokenRequest) error { } type jwtProfileKeySet struct { - storage jwtProfileKeyStorage + storage JWTProfileKeyStorage clientID string } diff --git a/pkg/op/verifier_jwt_profile_test.go b/pkg/op/verifier_jwt_profile_test.go new file mode 100644 index 0000000..d96cbb4 --- /dev/null +++ b/pkg/op/verifier_jwt_profile_test.go @@ -0,0 +1,117 @@ +package op_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + tu "github.com/zitadel/oidc/v3/internal/testutil" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" +) + +func TestNewJWTProfileVerifier(t *testing.T) { + want := &op.JWTProfileVerifier{ + Verifier: oidc.Verifier{ + Issuer: tu.ValidIssuer, + MaxAgeIAT: time.Minute, + Offset: time.Second, + }, + Storage: tu.JWTProfileKeyStorage{}, + } + got := op.NewJWTProfileVerifier(tu.JWTProfileKeyStorage{}, tu.ValidIssuer, time.Minute, time.Second, op.SubjectCheck(func(request *oidc.JWTTokenRequest) error { + return oidc.ErrSubjectMissing + })) + assert.Equal(t, want.Verifier, got.Verifier) + assert.Equal(t, want.Storage, got.Storage) + assert.ErrorIs(t, got.CheckSubject(nil), oidc.ErrSubjectMissing) +} + +func TestVerifyJWTAssertion(t *testing.T) { + errCtx, cancel := context.WithCancel(context.Background()) + cancel() + + verifier := op.NewJWTProfileVerifier(tu.JWTProfileKeyStorage{}, tu.ValidIssuer, time.Minute, 0) + tests := []struct { + name string + ctx context.Context + newToken func() (string, *oidc.JWTTokenRequest) + wantErr bool + }{ + { + name: "parse error", + ctx: context.Background(), + newToken: func() (string, *oidc.JWTTokenRequest) { return "!", nil }, + wantErr: true, + }, + { + name: "wrong audience", + ctx: context.Background(), + newToken: func() (string, *oidc.JWTTokenRequest) { + return tu.NewJWTProfileAssertion( + tu.ValidClientID, tu.ValidClientID, []string{"wrong"}, + time.Now(), tu.ValidExpiration, + ) + }, + wantErr: true, + }, + { + name: "expired", + ctx: context.Background(), + newToken: func() (string, *oidc.JWTTokenRequest) { + return tu.NewJWTProfileAssertion( + tu.ValidClientID, tu.ValidClientID, []string{tu.ValidIssuer}, + time.Now(), time.Now().Add(-time.Hour), + ) + }, + wantErr: true, + }, + { + name: "invalid iat", + ctx: context.Background(), + newToken: func() (string, *oidc.JWTTokenRequest) { + return tu.NewJWTProfileAssertion( + tu.ValidClientID, tu.ValidClientID, []string{tu.ValidIssuer}, + time.Now().Add(time.Hour), tu.ValidExpiration, + ) + }, + wantErr: true, + }, + { + name: "invalid subject", + ctx: context.Background(), + newToken: func() (string, *oidc.JWTTokenRequest) { + return tu.NewJWTProfileAssertion( + tu.ValidClientID, "wrong", []string{tu.ValidIssuer}, + time.Now(), tu.ValidExpiration, + ) + }, + wantErr: true, + }, + { + name: "check signature fail", + ctx: errCtx, + newToken: tu.ValidJWTProfileAssertion, + wantErr: true, + }, + { + name: "ok", + ctx: context.Background(), + newToken: tu.ValidJWTProfileAssertion, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertion, want := tt.newToken() + got, err := op.VerifyJWTAssertion(tt.ctx, assertion, verifier) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, want, got) + }) + } +} From 6af94fded0a1d5ddb448799358b029733b77d7a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Thu, 23 Mar 2023 16:31:38 +0200 Subject: [PATCH 206/502] feat: add context to all client calls (#345) BREAKING CHANGE closes #309 --- example/client/api/api.go | 3 +- example/client/app/app.go | 3 +- example/client/device/device.go | 4 +- example/client/service/service.go | 4 +- pkg/client/client.go | 30 ++++++------- pkg/client/integration_test.go | 30 ++++++++++--- pkg/client/jwt_profile.go | 5 ++- pkg/client/key.go | 8 ++-- pkg/client/profile/jwt_profile.go | 51 ++++++++++++++++------- pkg/client/rp/device.go | 4 +- pkg/client/rp/relying_party.go | 43 +++++-------------- pkg/client/rs/resource_server.go | 18 ++++---- pkg/client/tokenexchange/tokenexchange.go | 16 +++---- pkg/http/http.go | 4 +- 14 files changed, 124 insertions(+), 99 deletions(-) diff --git a/example/client/api/api.go b/example/client/api/api.go index 95e84e7..83ec2a1 100644 --- a/example/client/api/api.go +++ b/example/client/api/api.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "fmt" "log" @@ -27,7 +28,7 @@ func main() { port := os.Getenv("PORT") issuer := os.Getenv("ISSUER") - provider, err := rs.NewResourceServerFromKeyFile(issuer, keyPath) + provider, err := rs.NewResourceServerFromKeyFile(context.TODO(), issuer, keyPath) if err != nil { logrus.Fatalf("error creating provider %s", err.Error()) } diff --git a/example/client/app/app.go b/example/client/app/app.go index 446c17b..2cb5dfa 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "fmt" "net/http" @@ -43,7 +44,7 @@ func main() { options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath))) } - provider, err := rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...) + provider, err := rp.NewRelyingPartyOIDC(context.TODO(), issuer, clientID, clientSecret, redirectURI, scopes, options...) if err != nil { logrus.Fatalf("error creating provider %s", err.Error()) } diff --git a/example/client/device/device.go b/example/client/device/device.go index 88ecfe9..c186b34 100644 --- a/example/client/device/device.go +++ b/example/client/device/device.go @@ -39,13 +39,13 @@ func main() { options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath))) } - provider, err := rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, "", scopes, options...) + provider, err := rp.NewRelyingPartyOIDC(ctx, issuer, clientID, clientSecret, "", scopes, options...) if err != nil { logrus.Fatalf("error creating provider %s", err.Error()) } logrus.Info("starting device authorization flow") - resp, err := rp.DeviceAuthorization(scopes, provider) + resp, err := rp.DeviceAuthorization(ctx, scopes, provider) if err != nil { logrus.Fatal(err) } diff --git a/example/client/service/service.go b/example/client/service/service.go index 4908b09..ffcdccb 100644 --- a/example/client/service/service.go +++ b/example/client/service/service.go @@ -25,7 +25,7 @@ func main() { scopes := strings.Split(os.Getenv("SCOPES"), " ") if keyPath != "" { - ts, err := profile.NewJWTProfileTokenSourceFromKeyFile(issuer, keyPath, scopes) + ts, err := profile.NewJWTProfileTokenSourceFromKeyFile(context.TODO(), issuer, keyPath, scopes) if err != nil { logrus.Fatalf("error creating token source %s", err.Error()) } @@ -76,7 +76,7 @@ func main() { http.Error(w, err.Error(), http.StatusInternalServerError) return } - ts, err := profile.NewJWTProfileTokenSourceFromKeyFileData(issuer, key, scopes) + ts, err := profile.NewJWTProfileTokenSourceFromKeyFileData(context.TODO(), issuer, key, scopes) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/pkg/client/client.go b/pkg/client/client.go index e9af8ce..b9580ff 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -23,12 +23,12 @@ var Encoder = httphelper.Encoder(oidc.NewEncoder()) // Discover calls the discovery endpoint of the provided issuer and returns its configuration // It accepts an optional argument "wellknownUrl" which can be used to overide the dicovery endpoint url -func Discover(issuer string, httpClient *http.Client, wellKnownUrl ...string) (*oidc.DiscoveryConfiguration, error) { +func Discover(ctx context.Context, issuer string, httpClient *http.Client, wellKnownUrl ...string) (*oidc.DiscoveryConfiguration, error) { wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint if len(wellKnownUrl) == 1 && wellKnownUrl[0] != "" { wellKnown = wellKnownUrl[0] } - req, err := http.NewRequest("GET", wellKnown, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, wellKnown, nil) if err != nil { return nil, err } @@ -48,12 +48,12 @@ type TokenEndpointCaller interface { HttpClient() *http.Client } -func CallTokenEndpoint(request interface{}, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) { - return callTokenEndpoint(request, nil, caller) +func CallTokenEndpoint(ctx context.Context, request interface{}, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) { + return callTokenEndpoint(ctx, request, nil, caller) } -func callTokenEndpoint(request interface{}, authFn interface{}, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) { - req, err := httphelper.FormRequest(caller.TokenEndpoint(), request, Encoder, authFn) +func callTokenEndpoint(ctx context.Context, request interface{}, authFn interface{}, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) { + req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, authFn) if err != nil { return nil, err } @@ -74,8 +74,8 @@ type EndSessionCaller interface { HttpClient() *http.Client } -func CallEndSessionEndpoint(request interface{}, authFn interface{}, caller EndSessionCaller) (*url.URL, error) { - req, err := httphelper.FormRequest(caller.GetEndSessionEndpoint(), request, Encoder, authFn) +func CallEndSessionEndpoint(ctx context.Context, request interface{}, authFn interface{}, caller EndSessionCaller) (*url.URL, error) { + req, err := httphelper.FormRequest(ctx, caller.GetEndSessionEndpoint(), request, Encoder, authFn) if err != nil { return nil, err } @@ -117,8 +117,8 @@ type RevokeRequest struct { ClientSecret string `schema:"client_secret"` } -func CallRevokeEndpoint(request interface{}, authFn interface{}, caller RevokeCaller) error { - req, err := httphelper.FormRequest(caller.GetRevokeEndpoint(), request, Encoder, authFn) +func CallRevokeEndpoint(ctx context.Context, request interface{}, authFn interface{}, caller RevokeCaller) error { + req, err := httphelper.FormRequest(ctx, caller.GetRevokeEndpoint(), request, Encoder, authFn) if err != nil { return err } @@ -145,8 +145,8 @@ func CallRevokeEndpoint(request interface{}, authFn interface{}, caller RevokeCa return nil } -func CallTokenExchangeEndpoint(request interface{}, authFn interface{}, caller TokenEndpointCaller) (resp *oidc.TokenExchangeResponse, err error) { - req, err := httphelper.FormRequest(caller.TokenEndpoint(), request, Encoder, authFn) +func CallTokenExchangeEndpoint(ctx context.Context, request interface{}, authFn interface{}, caller TokenEndpointCaller) (resp *oidc.TokenExchangeResponse, err error) { + req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, authFn) if err != nil { return nil, err } @@ -186,8 +186,8 @@ type DeviceAuthorizationCaller interface { HttpClient() *http.Client } -func CallDeviceAuthorizationEndpoint(request *oidc.ClientCredentialsRequest, caller DeviceAuthorizationCaller) (*oidc.DeviceAuthorizationResponse, error) { - req, err := httphelper.FormRequest(caller.GetDeviceAuthorizationEndpoint(), request, Encoder, nil) +func CallDeviceAuthorizationEndpoint(ctx context.Context, request *oidc.ClientCredentialsRequest, caller DeviceAuthorizationCaller) (*oidc.DeviceAuthorizationResponse, error) { + req, err := httphelper.FormRequest(ctx, caller.GetDeviceAuthorizationEndpoint(), request, Encoder, nil) if err != nil { return nil, err } @@ -208,7 +208,7 @@ type DeviceAccessTokenRequest struct { } func CallDeviceAccessTokenEndpoint(ctx context.Context, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) { - req, err := httphelper.FormRequest(caller.TokenEndpoint(), request, Encoder, nil) + req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, nil) if err != nil { return nil, err } diff --git a/pkg/client/integration_test.go b/pkg/client/integration_test.go index 709d5a1..2c3ef62 100644 --- a/pkg/client/integration_test.go +++ b/pkg/client/integration_test.go @@ -2,6 +2,7 @@ package client_test import ( "bytes" + "context" "io" "io/ioutil" "math/rand" @@ -10,7 +11,9 @@ import ( "net/http/httptest" "net/url" "os" + "os/signal" "strconv" + "syscall" "testing" "time" @@ -27,6 +30,18 @@ import ( "github.com/zitadel/oidc/v3/pkg/oidc" ) +var CTX context.Context + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT) + defer cancel() + CTX, cancel = context.WithTimeout(ctx, time.Minute) + defer cancel() + return m.Run() + }()) +} + func TestRelyingPartySession(t *testing.T) { t.Log("------- start example OP ------") targetURL := "http://local-site" @@ -45,7 +60,7 @@ func TestRelyingPartySession(t *testing.T) { t.Log("------- refresh tokens ------") - newTokens, err := rp.RefreshAccessToken(provider, refreshToken, "", "") + newTokens, err := rp.RefreshAccessToken(CTX, provider, refreshToken, "", "") require.NoError(t, err, "refresh token") assert.NotNil(t, newTokens, "access token") t.Logf("new access token %s", newTokens.AccessToken) @@ -56,7 +71,7 @@ func TestRelyingPartySession(t *testing.T) { t.Log("------ end session (logout) ------") - newLoc, err := rp.EndSession(provider, idToken, "", "") + newLoc, err := rp.EndSession(CTX, provider, idToken, "", "") require.NoError(t, err, "logout") if newLoc != nil { t.Logf("redirect to %s", newLoc) @@ -66,11 +81,11 @@ func TestRelyingPartySession(t *testing.T) { t.Log("------ attempt refresh again (should fail) ------") t.Log("trying original refresh token", refreshToken) - _, err = rp.RefreshAccessToken(provider, refreshToken, "", "") + _, err = rp.RefreshAccessToken(CTX, provider, refreshToken, "", "") assert.Errorf(t, err, "refresh with original") if newTokens.RefreshToken != "" { t.Log("trying replacement refresh token", newTokens.RefreshToken) - _, err = rp.RefreshAccessToken(provider, newTokens.RefreshToken, "", "") + _, err = rp.RefreshAccessToken(CTX, provider, newTokens.RefreshToken, "", "") assert.Errorf(t, err, "refresh with replacement") } } @@ -92,12 +107,13 @@ func TestResourceServerTokenExchange(t *testing.T) { t.Log("------- run authorization code flow ------") provider, _, refreshToken, idToken := RunAuthorizationCodeFlow(t, opServer, clientID, clientSecret) - resourceServer, err := rs.NewResourceServerClientCredentials(opServer.URL, clientID, clientSecret) + resourceServer, err := rs.NewResourceServerClientCredentials(CTX, opServer.URL, clientID, clientSecret) require.NoError(t, err, "new resource server") t.Log("------- exchage refresh tokens (impersonation) ------") tokenExchangeResponse, err := tokenexchange.ExchangeToken( + CTX, resourceServer, refreshToken, oidc.RefreshTokenType, @@ -117,7 +133,7 @@ func TestResourceServerTokenExchange(t *testing.T) { t.Log("------ end session (logout) ------") - newLoc, err := rp.EndSession(provider, idToken, "", "") + newLoc, err := rp.EndSession(CTX, provider, idToken, "", "") require.NoError(t, err, "logout") if newLoc != nil { t.Logf("redirect to %s", newLoc) @@ -128,6 +144,7 @@ func TestResourceServerTokenExchange(t *testing.T) { t.Log("------- attempt exchage again (should fail) ------") tokenExchangeResponse, err = tokenexchange.ExchangeToken( + CTX, resourceServer, refreshToken, oidc.RefreshTokenType, @@ -166,6 +183,7 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID, key := []byte("test1234test1234") cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure()) provider, err = rp.NewRelyingPartyOIDC( + CTX, opServer.URL, clientID, clientSecret, diff --git a/pkg/client/jwt_profile.go b/pkg/client/jwt_profile.go index 486d998..0a5d9ec 100644 --- a/pkg/client/jwt_profile.go +++ b/pkg/client/jwt_profile.go @@ -1,6 +1,7 @@ package client import ( + "context" "net/url" "golang.org/x/oauth2" @@ -10,8 +11,8 @@ import ( ) // JWTProfileExchange handles the oauth2 jwt profile exchange -func JWTProfileExchange(jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, caller TokenEndpointCaller) (*oauth2.Token, error) { - return CallTokenEndpoint(jwtProfileGrantRequest, caller) +func JWTProfileExchange(ctx context.Context, jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, caller TokenEndpointCaller) (*oauth2.Token, error) { + return CallTokenEndpoint(ctx, jwtProfileGrantRequest, caller) } func ClientAssertionCodeOptions(assertion string) []oauth2.AuthCodeOption { diff --git a/pkg/client/key.go b/pkg/client/key.go index 740c6d3..0c01dd2 100644 --- a/pkg/client/key.go +++ b/pkg/client/key.go @@ -10,7 +10,7 @@ const ( applicationKey = "application" ) -type keyFile struct { +type KeyFile struct { Type string `json:"type"` // serviceaccount or application KeyID string `json:"keyId"` Key string `json:"key"` @@ -23,7 +23,7 @@ type keyFile struct { ClientID string `json:"clientId"` } -func ConfigFromKeyFile(path string) (*keyFile, error) { +func ConfigFromKeyFile(path string) (*KeyFile, error) { data, err := ioutil.ReadFile(path) if err != nil { return nil, err @@ -31,8 +31,8 @@ func ConfigFromKeyFile(path string) (*keyFile, error) { return ConfigFromKeyFileData(data) } -func ConfigFromKeyFileData(data []byte) (*keyFile, error) { - var f keyFile +func ConfigFromKeyFileData(data []byte) (*KeyFile, error) { + var f KeyFile if err := json.Unmarshal(data, &f); err != nil { return nil, err } diff --git a/pkg/client/profile/jwt_profile.go b/pkg/client/profile/jwt_profile.go index bb18570..668f749 100644 --- a/pkg/client/profile/jwt_profile.go +++ b/pkg/client/profile/jwt_profile.go @@ -1,6 +1,7 @@ package profile import ( + "context" "net/http" "time" @@ -11,9 +12,12 @@ import ( "github.com/zitadel/oidc/v3/pkg/oidc" ) -// jwtProfileTokenSource implement the oauth2.TokenSource -// it will request a token using the OAuth2 JWT Profile Grant -// therefore sending an `assertion` by singing a JWT with the provided private key +type TokenSource interface { + oauth2.TokenSource + TokenCtx(context.Context) (*oauth2.Token, error) +} + +// jwtProfileTokenSource implements the TokenSource type jwtProfileTokenSource struct { clientID string audience []string @@ -23,23 +27,38 @@ type jwtProfileTokenSource struct { tokenEndpoint string } -func NewJWTProfileTokenSourceFromKeyFile(issuer, keyPath string, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) { - keyData, err := client.ConfigFromKeyFile(keyPath) +// NewJWTProfileTokenSourceFromKeyFile returns an implementation of TokenSource +// It will request a token using the OAuth2 JWT Profile Grant, +// therefore sending an `assertion` by singing a JWT with the provided private key from jsonFile. +// +// The passed context is only used for the call to the Discover endpoint. +func NewJWTProfileTokenSourceFromKeyFile(ctx context.Context, issuer, jsonFile string, scopes []string, options ...func(source *jwtProfileTokenSource)) (TokenSource, error) { + keyData, err := client.ConfigFromKeyFile(jsonFile) if err != nil { return nil, err } - return NewJWTProfileTokenSource(issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...) + return NewJWTProfileTokenSource(ctx, issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...) } -func NewJWTProfileTokenSourceFromKeyFileData(issuer string, data []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) { - keyData, err := client.ConfigFromKeyFileData(data) +// NewJWTProfileTokenSourceFromKeyFileData returns an implementation of oauth2.TokenSource +// It will request a token using the OAuth2 JWT Profile Grant, +// therefore sending an `assertion` by singing a JWT with the provided private key in jsonData. +// +// The passed context is only used for the call to the Discover endpoint. +func NewJWTProfileTokenSourceFromKeyFileData(ctx context.Context, issuer string, jsonData []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (TokenSource, error) { + keyData, err := client.ConfigFromKeyFileData(jsonData) if err != nil { return nil, err } - return NewJWTProfileTokenSource(issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...) + return NewJWTProfileTokenSource(ctx, issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...) } -func NewJWTProfileTokenSource(issuer, clientID, keyID string, key []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) { +// NewJWTProfileSource returns an implementation of oauth2.TokenSource +// It will request a token using the OAuth2 JWT Profile Grant, +// therefore sending an `assertion` by singing a JWT with the provided private key. +// +// The passed context is only used for the call to the Discover endpoint. +func NewJWTProfileTokenSource(ctx context.Context, issuer, clientID, keyID string, key []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (TokenSource, error) { signer, err := client.NewSignerFromPrivateKeyByte(key, keyID) if err != nil { return nil, err @@ -55,7 +74,7 @@ func NewJWTProfileTokenSource(issuer, clientID, keyID string, key []byte, scopes opt(source) } if source.tokenEndpoint == "" { - config, err := client.Discover(issuer, source.httpClient) + config, err := client.Discover(ctx, issuer, source.httpClient) if err != nil { return nil, err } @@ -64,13 +83,13 @@ func NewJWTProfileTokenSource(issuer, clientID, keyID string, key []byte, scopes return source, nil } -func WithHTTPClient(client *http.Client) func(*jwtProfileTokenSource) { +func WithHTTPClient(client *http.Client) func(source *jwtProfileTokenSource) { return func(source *jwtProfileTokenSource) { source.httpClient = client } } -func WithStaticTokenEndpoint(issuer, tokenEndpoint string) func(*jwtProfileTokenSource) { +func WithStaticTokenEndpoint(issuer, tokenEndpoint string) func(source *jwtProfileTokenSource) { return func(source *jwtProfileTokenSource) { source.tokenEndpoint = tokenEndpoint } @@ -85,9 +104,13 @@ func (j *jwtProfileTokenSource) HttpClient() *http.Client { } func (j *jwtProfileTokenSource) Token() (*oauth2.Token, error) { + return j.TokenCtx(context.Background()) +} + +func (j *jwtProfileTokenSource) TokenCtx(ctx context.Context) (*oauth2.Token, error) { assertion, err := client.SignedJWTProfileAssertion(j.clientID, j.audience, time.Hour, j.signer) if err != nil { return nil, err } - return client.JWTProfileExchange(oidc.NewJWTProfileGrantRequest(assertion, j.scopes...), j) + return client.JWTProfileExchange(ctx, oidc.NewJWTProfileGrantRequest(assertion, j.scopes...), j) } diff --git a/pkg/client/rp/device.go b/pkg/client/rp/device.go index 9cfc41e..b2c5be6 100644 --- a/pkg/client/rp/device.go +++ b/pkg/client/rp/device.go @@ -33,13 +33,13 @@ func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc. // DeviceAuthorization starts a new Device Authorization flow as defined // in RFC 8628, section 3.1 and 3.2: // https://www.rfc-editor.org/rfc/rfc8628#section-3.1 -func DeviceAuthorization(scopes []string, rp RelyingParty) (*oidc.DeviceAuthorizationResponse, error) { +func DeviceAuthorization(ctx context.Context, scopes []string, rp RelyingParty) (*oidc.DeviceAuthorizationResponse, error) { req, err := newDeviceClientCredentialsRequest(scopes, rp) if err != nil { return nil, err } - return client.CallDeviceAuthorizationEndpoint(req, rp) + return client.CallDeviceAuthorizationEndpoint(ctx, req, rp) } // DeviceAccessToken attempts to obtain tokens from a Device Authorization, diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index bd96e16..820107f 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -7,7 +7,6 @@ import ( "fmt" "net/http" "net/url" - "strings" "time" "github.com/google/uuid" @@ -177,7 +176,7 @@ func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingPart // NewRelyingPartyOIDC creates an (OIDC) RelyingParty with the given // issuer, clientID, clientSecret, redirectURI, scopes and possible configOptions // it will run discovery on the provided issuer and use the found endpoints -func NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelyingParty, error) { +func NewRelyingPartyOIDC(ctx context.Context, issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelyingParty, error) { rp := &relyingParty{ issuer: issuer, oauthConfig: &oauth2.Config{ @@ -195,7 +194,7 @@ func NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, sco return nil, err } } - discoveryConfiguration, err := client.Discover(rp.issuer, rp.httpClient, rp.DiscoveryEndpoint) + discoveryConfiguration, err := client.Discover(ctx, rp.issuer, rp.httpClient, rp.DiscoveryEndpoint) if err != nil { return nil, err } @@ -310,26 +309,6 @@ func SignerFromKeyAndKeyID(key []byte, keyID string) SignerFromKey { } } -// Discover calls the discovery endpoint of the provided issuer and returns the found endpoints -// -// deprecated: use client.Discover -func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { - wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint - req, err := http.NewRequest("GET", wellKnown, nil) - if err != nil { - return Endpoints{}, err - } - discoveryConfig := new(oidc.DiscoveryConfiguration) - err = httphelper.HttpRequest(httpClient, req, &discoveryConfig) - if err != nil { - return Endpoints{}, err - } - if discoveryConfig.Issuer != issuer { - return Endpoints{}, oidc.ErrIssuerInvalid - } - return GetEndpoints(discoveryConfig), nil -} - // AuthURL returns the auth request url // (wrapping the oauth2 `AuthCodeURL`) func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string { @@ -463,7 +442,7 @@ type CodeExchangeUserinfoCallback[C oidc.IDClaims] func(w http.ResponseWriter, r // on success it will pass the userinfo into its callback function as well func UserinfoCallback[C oidc.IDClaims](f CodeExchangeUserinfoCallback[C]) CodeExchangeCallback[C] { return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) { - info, err := Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp) + info, err := Userinfo(r.Context(), tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp) if err != nil { http.Error(w, "userinfo failed: "+err.Error(), http.StatusUnauthorized) return @@ -473,8 +452,8 @@ func UserinfoCallback[C oidc.IDClaims](f CodeExchangeUserinfoCallback[C]) CodeEx } // Userinfo will call the OIDC Userinfo Endpoint with the provided token -func Userinfo(token, tokenType, subject string, rp RelyingParty) (*oidc.UserInfo, error) { - req, err := http.NewRequest("GET", rp.UserinfoEndpoint(), nil) +func Userinfo(ctx context.Context, token, tokenType, subject string, rp RelyingParty) (*oidc.UserInfo, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, rp.UserinfoEndpoint(), nil) if err != nil { return nil, err } @@ -620,7 +599,7 @@ type RefreshTokenRequest struct { GrantType oidc.GrantType `schema:"grant_type"` } -func RefreshAccessToken(rp RelyingParty, refreshToken, clientAssertion, clientAssertionType string) (*oauth2.Token, error) { +func RefreshAccessToken(ctx context.Context, rp RelyingParty, refreshToken, clientAssertion, clientAssertionType string) (*oauth2.Token, error) { request := RefreshTokenRequest{ RefreshToken: refreshToken, Scopes: rp.OAuthConfig().Scopes, @@ -630,17 +609,17 @@ func RefreshAccessToken(rp RelyingParty, refreshToken, clientAssertion, clientAs ClientAssertionType: clientAssertionType, GrantType: oidc.GrantTypeRefreshToken, } - return client.CallTokenEndpoint(request, tokenEndpointCaller{RelyingParty: rp}) + return client.CallTokenEndpoint(ctx, request, tokenEndpointCaller{RelyingParty: rp}) } -func EndSession(rp RelyingParty, idToken, optionalRedirectURI, optionalState string) (*url.URL, error) { +func EndSession(ctx context.Context, rp RelyingParty, idToken, optionalRedirectURI, optionalState string) (*url.URL, error) { request := oidc.EndSessionRequest{ IdTokenHint: idToken, ClientID: rp.OAuthConfig().ClientID, PostLogoutRedirectURI: optionalRedirectURI, State: optionalState, } - return client.CallEndSessionEndpoint(request, nil, rp) + return client.CallEndSessionEndpoint(ctx, request, nil, rp) } // RevokeToken requires a RelyingParty that is also a client.RevokeCaller. The RelyingParty @@ -648,7 +627,7 @@ func EndSession(rp RelyingParty, idToken, optionalRedirectURI, optionalState str // NewRelyingPartyOAuth() does not. // // tokenTypeHint should be either "id_token" or "refresh_token". -func RevokeToken(rp RelyingParty, token string, tokenTypeHint string) error { +func RevokeToken(ctx context.Context, rp RelyingParty, token string, tokenTypeHint string) error { request := client.RevokeRequest{ Token: token, TokenTypeHint: tokenTypeHint, @@ -656,7 +635,7 @@ func RevokeToken(rp RelyingParty, token string, tokenTypeHint string) error { ClientSecret: rp.OAuthConfig().ClientSecret, } if rc, ok := rp.(client.RevokeCaller); ok && rc.GetRevokeEndpoint() != "" { - return client.CallRevokeEndpoint(request, nil, rc) + return client.CallRevokeEndpoint(ctx, request, nil, rc) } return fmt.Errorf("RelyingParty does not support RevokeCaller") } diff --git a/pkg/client/rs/resource_server.go b/pkg/client/rs/resource_server.go index f0e0e0a..054dfbe 100644 --- a/pkg/client/rs/resource_server.go +++ b/pkg/client/rs/resource_server.go @@ -42,14 +42,14 @@ func (r *resourceServer) AuthFn() (interface{}, error) { return r.authFn() } -func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option ...Option) (ResourceServer, error) { +func NewResourceServerClientCredentials(ctx context.Context, issuer, clientID, clientSecret string, option ...Option) (ResourceServer, error) { authorizer := func() (interface{}, error) { return httphelper.AuthorizeBasic(clientID, clientSecret), nil } - return newResourceServer(issuer, authorizer, option...) + return newResourceServer(ctx, issuer, authorizer, option...) } -func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...Option) (ResourceServer, error) { +func NewResourceServerJWTProfile(ctx context.Context, issuer, clientID, keyID string, key []byte, options ...Option) (ResourceServer, error) { signer, err := client.NewSignerFromPrivateKeyByte(key, keyID) if err != nil { return nil, err @@ -61,10 +61,10 @@ func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, opt } return client.ClientAssertionFormAuthorization(assertion), nil } - return newResourceServer(issuer, authorizer, options...) + return newResourceServer(ctx, issuer, authorizer, options...) } -func newResourceServer(issuer string, authorizer func() (interface{}, error), options ...Option) (*resourceServer, error) { +func newResourceServer(ctx context.Context, issuer string, authorizer func() (interface{}, error), options ...Option) (*resourceServer, error) { rs := &resourceServer{ issuer: issuer, httpClient: httphelper.DefaultHTTPClient, @@ -73,7 +73,7 @@ func newResourceServer(issuer string, authorizer func() (interface{}, error), op optFunc(rs) } if rs.introspectURL == "" || rs.tokenURL == "" { - config, err := client.Discover(rs.issuer, rs.httpClient) + config, err := client.Discover(ctx, rs.issuer, rs.httpClient) if err != nil { return nil, err } @@ -87,12 +87,12 @@ func newResourceServer(issuer string, authorizer func() (interface{}, error), op return rs, nil } -func NewResourceServerFromKeyFile(issuer, path string, options ...Option) (ResourceServer, error) { +func NewResourceServerFromKeyFile(ctx context.Context, issuer, path string, options ...Option) (ResourceServer, error) { c, err := client.ConfigFromKeyFile(path) if err != nil { return nil, err } - return NewResourceServerJWTProfile(issuer, c.ClientID, c.KeyID, []byte(c.Key), options...) + return NewResourceServerJWTProfile(ctx, issuer, c.ClientID, c.KeyID, []byte(c.Key), options...) } type Option func(*resourceServer) @@ -117,7 +117,7 @@ func Introspect(ctx context.Context, rp ResourceServer, token string) (*oidc.Int if err != nil { return nil, err } - req, err := httphelper.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, client.Encoder, authFn) + req, err := httphelper.FormRequest(ctx, rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, client.Encoder, authFn) if err != nil { return nil, err } diff --git a/pkg/client/tokenexchange/tokenexchange.go b/pkg/client/tokenexchange/tokenexchange.go index ce665cd..1c10df2 100644 --- a/pkg/client/tokenexchange/tokenexchange.go +++ b/pkg/client/tokenexchange/tokenexchange.go @@ -1,6 +1,7 @@ package tokenexchange import ( + "context" "errors" "net/http" @@ -21,18 +22,18 @@ type OAuthTokenExchange struct { authFn func() (interface{}, error) } -func NewTokenExchanger(issuer string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) { - return newOAuthTokenExchange(issuer, nil, options...) +func NewTokenExchanger(ctx context.Context, issuer string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) { + return newOAuthTokenExchange(ctx, issuer, nil, options...) } -func NewTokenExchangerClientCredentials(issuer, clientID, clientSecret string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) { +func NewTokenExchangerClientCredentials(ctx context.Context, issuer, clientID, clientSecret string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) { authorizer := func() (interface{}, error) { return httphelper.AuthorizeBasic(clientID, clientSecret), nil } - return newOAuthTokenExchange(issuer, authorizer, options...) + return newOAuthTokenExchange(ctx, issuer, authorizer, options...) } -func newOAuthTokenExchange(issuer string, authorizer func() (interface{}, error), options ...func(source *OAuthTokenExchange)) (*OAuthTokenExchange, error) { +func newOAuthTokenExchange(ctx context.Context, issuer string, authorizer func() (interface{}, error), options ...func(source *OAuthTokenExchange)) (*OAuthTokenExchange, error) { te := &OAuthTokenExchange{ httpClient: httphelper.DefaultHTTPClient, } @@ -41,7 +42,7 @@ func newOAuthTokenExchange(issuer string, authorizer func() (interface{}, error) } if te.tokenEndpoint == "" { - config, err := client.Discover(issuer, te.httpClient) + config, err := client.Discover(ctx, issuer, te.httpClient) if err != nil { return nil, err } @@ -89,6 +90,7 @@ func (te *OAuthTokenExchange) AuthFn() (interface{}, error) { // ExchangeToken sends a token exchange request (rfc 8693) to te's token endpoint. // SubjectToken and SubjectTokenType are required parameters. func ExchangeToken( + ctx context.Context, te TokenExchanger, SubjectToken string, SubjectTokenType oidc.TokenType, @@ -123,5 +125,5 @@ func ExchangeToken( RequestedTokenType: RequestedTokenType, } - return client.CallTokenExchangeEndpoint(request, authFn, te) + return client.CallTokenExchangeEndpoint(ctx, request, authFn, te) } diff --git a/pkg/http/http.go b/pkg/http/http.go index d3c5b4f..9771888 100644 --- a/pkg/http/http.go +++ b/pkg/http/http.go @@ -33,7 +33,7 @@ func AuthorizeBasic(user, password string) RequestAuthorization { } } -func FormRequest(endpoint string, request interface{}, encoder Encoder, authFn interface{}) (*http.Request, error) { +func FormRequest(ctx context.Context, endpoint string, request interface{}, encoder Encoder, authFn interface{}) (*http.Request, error) { form := url.Values{} if err := encoder.Encode(request, form); err != nil { return nil, err @@ -42,7 +42,7 @@ func FormRequest(endpoint string, request interface{}, encoder Encoder, authFn i fn(form) } body := strings.NewReader(form.Encode()) - req, err := http.NewRequest("POST", endpoint, body) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body) if err != nil { return nil, err } From c9555c7f1bb238b4ea48f5cbfa2332c5ef517d08 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Fri, 24 Mar 2023 09:55:41 -0700 Subject: [PATCH 207/502] feat: add CanSetUserinfoFromRequest interface (#347) --- example/server/storage/storage.go | 13 ++++++++++--- example/server/storage/storage_dynamic.go | 15 +++++++++++++-- pkg/op/storage.go | 9 +++++++++ pkg/op/token.go | 6 ++++++ 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index 7e1afbd..acac571 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -438,10 +438,17 @@ func (s *Storage) AuthorizeClientIDSecret(ctx context.Context, clientID, clientS return nil } -// SetUserinfoFromScopes implements the op.Storage interface -// it will be called for the creation of an id_token, so we'll just pass it to the private function without any further check +// SetUserinfoFromScopes implements the op.Storage interface. +// Provide an empty implementation and use SetUserinfoFromRequest instead. func (s *Storage) SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error { - return s.setUserinfo(ctx, userinfo, userID, clientID, scopes) + return nil +} + +// SetUserinfoFromRequests implements the op.CanSetUserinfoFromRequest interface. In the +// next major release, it will be required for op.Storage. +// It will be called for the creation of an id_token, so we'll just pass it to the private function without any further check +func (s *Storage) SetUserinfoFromRequest(ctx context.Context, userinfo *oidc.UserInfo, token op.IDTokenRequest, scopes []string) error { + return s.setUserinfo(ctx, userinfo, token.GetSubject(), token.GetClientID(), scopes) } // SetUserinfoFromToken implements the op.Storage interface diff --git a/example/server/storage/storage_dynamic.go b/example/server/storage/storage_dynamic.go index 6e5ee32..07af903 100644 --- a/example/server/storage/storage_dynamic.go +++ b/example/server/storage/storage_dynamic.go @@ -196,8 +196,8 @@ func (s *multiStorage) AuthorizeClientIDSecret(ctx context.Context, clientID, cl return storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret) } -// SetUserinfoFromScopes implements the op.Storage interface -// it will be called for the creation of an id_token, so we'll just pass it to the private function without any further check +// SetUserinfoFromScopes implements the op.Storage interface. +// Provide an empty implementation and use SetUserinfoFromRequest instead. func (s *multiStorage) SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error { storage, err := s.storageFromContext(ctx) if err != nil { @@ -206,6 +206,17 @@ func (s *multiStorage) SetUserinfoFromScopes(ctx context.Context, userinfo *oidc return storage.SetUserinfoFromScopes(ctx, userinfo, userID, clientID, scopes) } +// SetUserinfoFromRequests implements the op.CanSetUserinfoFromRequest interface. In the +// next major release, it will be required for op.Storage. +// It will be called for the creation of an id_token, so we'll just pass it to the private function without any further check +func (s *multiStorage) SetUserinfoFromRequest(ctx context.Context, userinfo *oidc.UserInfo, token op.IDTokenRequest, scopes []string) error { + storage, err := s.storageFromContext(ctx) + if err != nil { + return err + } + return storage.SetUserinfoFromRequest(ctx, userinfo, token, scopes) +} + // SetUserinfoFromToken implements the op.Storage interface // it will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function func (s *multiStorage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, subject, origin string) error { diff --git a/pkg/op/storage.go b/pkg/op/storage.go index e36eac7..590c4a0 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -113,6 +113,8 @@ type OPStorage interface { // handle the current request. GetClientByClientID(ctx context.Context, clientID string) (Client, error) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error + // SetUserinfoFromScopes is deprecated and should have an empty implementation for now. + // Implement SetUserinfoFromRequest instead. SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, subject, origin string) error SetIntrospectionFromToken(ctx context.Context, userinfo *oidc.IntrospectionResponse, tokenID, subject, clientID string) error @@ -127,6 +129,13 @@ type JWTProfileTokenStorage interface { JWTProfileTokenType(ctx context.Context, request TokenRequest) (AccessTokenType, error) } +// CanSetUserinfoFromRequest is an optional additional interface that may be implemented by +// implementors of Storage. It allows additional data to be set in id_tokens based on the +// request. +type CanSetUserinfoFromRequest interface { + SetUserinfoFromRequest(ctx context.Context, userinfo *oidc.UserInfo, request IDTokenRequest, scopes []string) error +} + // Storage is a required parameter for NewOpenIDProvider(). In addition to the // embedded interfaces below, if the passed Storage implements ClientCredentialsStorage // then the grant type "client_credentials" will be supported. In that case, the access diff --git a/pkg/op/token.go b/pkg/op/token.go index 58568a7..6dfc993 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -190,6 +190,12 @@ func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, v if err != nil { return "", err } + if fromRequest, ok := storage.(CanSetUserinfoFromRequest); ok { + err := fromRequest.SetUserinfoFromRequest(ctx, userInfo, request, scopes) + if err != nil { + return "", err + } + } claims.SetUserInfo(userInfo) } if code != "" { From be3cc13c27aad3a778bb46258b84293836b70e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Mon, 27 Mar 2023 16:41:09 +0300 Subject: [PATCH 208/502] fix: merge user info claims into id token claims (#349) oidc IDTokenClaims.SetUserInfo did not set the claims map from user info. This fix merges the claims map into the IDToken Claims map. --- pkg/oidc/token.go | 8 +++++++- pkg/oidc/token_test.go | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index 127db97..776e758 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -8,6 +8,7 @@ import ( "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" + "github.com/muhlemmer/gu" "github.com/zitadel/oidc/v2/pkg/crypto" ) @@ -157,6 +158,11 @@ func (t *IDTokenClaims) SetUserInfo(i *UserInfo) { t.UserInfoEmail = i.UserInfoEmail t.UserInfoPhone = i.UserInfoPhone t.Address = i.Address + + if t.Claims == nil { + t.Claims = make(map[string]any, len(t.Claims)) + } + gu.MapMerge(i.Claims, t.Claims) } func (t *IDTokenClaims) GetUserInfo() *UserInfo { @@ -166,7 +172,7 @@ func (t *IDTokenClaims) GetUserInfo() *UserInfo { UserInfoEmail: t.UserInfoEmail, UserInfoPhone: t.UserInfoPhone, Address: t.Address, - Claims: t.Claims, + Claims: gu.MapCopy(t.Claims), } } diff --git a/pkg/oidc/token_test.go b/pkg/oidc/token_test.go index 8dcfc7e..7377a84 100644 --- a/pkg/oidc/token_test.go +++ b/pkg/oidc/token_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "golang.org/x/text/language" "gopkg.in/square/go-jose.v2" @@ -181,6 +182,7 @@ func TestIDTokenClaims_SetUserInfo(t *testing.T) { UserInfoEmail: userInfoData.UserInfoEmail, UserInfoPhone: userInfoData.UserInfoPhone, Address: userInfoData.Address, + Claims: gu.MapCopy(userInfoData.Claims), } var got IDTokenClaims From e1d50faf9b58798a0b4e397fea37a93a464ac5ba Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Mon, 27 Mar 2023 13:40:10 -0700 Subject: [PATCH 209/502] fix: do not modify userInfo when marshaling --- pkg/oidc/introspection_test.go | 3 ++- pkg/oidc/regression_assert_test.go | 7 +++++-- pkg/oidc/token.go | 1 - pkg/oidc/token_test.go | 5 +++-- pkg/oidc/userinfo_test.go | 5 ++++- pkg/oidc/util.go | 13 +++++++++---- 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/pkg/oidc/introspection_test.go b/pkg/oidc/introspection_test.go index bd49894..60cf8a4 100644 --- a/pkg/oidc/introspection_test.go +++ b/pkg/oidc/introspection_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "testing" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -25,7 +26,7 @@ func TestIntrospectionResponse_SetUserInfo(t *testing.T) { UserInfoProfile: userInfoData.UserInfoProfile, UserInfoEmail: userInfoData.UserInfoEmail, UserInfoPhone: userInfoData.UserInfoPhone, - Claims: userInfoData.Claims, + Claims: gu.MapCopy(userInfoData.Claims), }, }, { diff --git a/pkg/oidc/regression_assert_test.go b/pkg/oidc/regression_assert_test.go index 5e9fb3d..dd9f5ad 100644 --- a/pkg/oidc/regression_assert_test.go +++ b/pkg/oidc/regression_assert_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "io" "os" + "reflect" "strings" "testing" @@ -38,10 +39,12 @@ func Test_assert_regression(t *testing.T) { assert.JSONEq(t, want, first) + target := reflect.New(reflect.TypeOf(obj).Elem()).Interface() + require.NoError(t, - json.Unmarshal([]byte(first), obj), + json.Unmarshal([]byte(first), target), ) - second, err := json.Marshal(obj) + second, err := json.Marshal(target) require.NoError(t, err) assert.JSONEq(t, want, string(second)) diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index 776e758..5283eb5 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -158,7 +158,6 @@ func (t *IDTokenClaims) SetUserInfo(i *UserInfo) { t.UserInfoEmail = i.UserInfoEmail t.UserInfoPhone = i.UserInfoPhone t.Address = i.Address - if t.Claims == nil { t.Claims = make(map[string]any, len(t.Claims)) } diff --git a/pkg/oidc/token_test.go b/pkg/oidc/token_test.go index 7377a84..ef1e77f 100644 --- a/pkg/oidc/token_test.go +++ b/pkg/oidc/token_test.go @@ -4,7 +4,6 @@ import ( "testing" "time" - "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "golang.org/x/text/language" "gopkg.in/square/go-jose.v2" @@ -182,7 +181,9 @@ func TestIDTokenClaims_SetUserInfo(t *testing.T) { UserInfoEmail: userInfoData.UserInfoEmail, UserInfoPhone: userInfoData.UserInfoPhone, Address: userInfoData.Address, - Claims: gu.MapCopy(userInfoData.Claims), + Claims: map[string]interface{}{ + "foo": "bar", + }, } var got IDTokenClaims diff --git a/pkg/oidc/userinfo_test.go b/pkg/oidc/userinfo_test.go index faab4e3..a574366 100644 --- a/pkg/oidc/userinfo_test.go +++ b/pkg/oidc/userinfo_test.go @@ -52,11 +52,14 @@ func TestUserInfoMarshal(t *testing.T) { out := new(UserInfo) assert.NoError(t, json.Unmarshal(marshal, out)) - assert.Equal(t, userinfo, out) expected, err := json.Marshal(out) assert.NoError(t, err) assert.Equal(t, expected, marshal) + + out2 := new(UserInfo) + assert.NoError(t, json.Unmarshal(expected, out2)) + assert.Equal(t, out, out2) } func TestUserInfoEmailVerifiedUnmarshal(t *testing.T) { diff --git a/pkg/oidc/util.go b/pkg/oidc/util.go index a89d75e..462ea44 100644 --- a/pkg/oidc/util.go +++ b/pkg/oidc/util.go @@ -9,7 +9,7 @@ import ( // mergeAndMarshalClaims merges registered and the custom // claims map into a single JSON object. // Registered fields overwrite custom claims. -func mergeAndMarshalClaims(registered any, claims map[string]any) ([]byte, error) { +func mergeAndMarshalClaims(registered any, extraClaims map[string]any) ([]byte, error) { // Use a buffer for memory re-use, instead off letting // json allocate a new []byte for every step. buf := new(bytes.Buffer) @@ -19,16 +19,21 @@ func mergeAndMarshalClaims(registered any, claims map[string]any) ([]byte, error return nil, fmt.Errorf("oidc registered claims: %w", err) } - if len(claims) > 0 { + if len(extraClaims) > 0 { + merged := make(map[string]any) + for k, v := range extraClaims { + merged[k] = v + } + // Merge JSON data into custom claims. // The full-read action by the decoder resets the buffer // to zero len, while retaining underlaying cap. - if err := json.NewDecoder(buf).Decode(&claims); err != nil { + if err := json.NewDecoder(buf).Decode(&merged); err != nil { return nil, fmt.Errorf("oidc registered claims: %w", err) } // Marshal the final result. - if err := json.NewEncoder(buf).Encode(claims); err != nil { + if err := json.NewEncoder(buf).Encode(merged); err != nil { return nil, fmt.Errorf("oidc custom claims: %w", err) } } From adebbe4c32e0e30ed6dc94b372e8c1f0fcbd53aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 28 Mar 2023 14:57:27 +0300 Subject: [PATCH 210/502] chore: replace gorilla/schema with zitadel/schema (#348) Fixes #302 --- go.mod | 2 +- go.sum | 4 ++-- pkg/oidc/types.go | 2 +- pkg/oidc/types_test.go | 2 +- pkg/op/auth_request_test.go | 2 +- pkg/op/client_test.go | 2 +- pkg/op/mock/authorizer.mock.impl.go | 2 +- pkg/op/op.go | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 7cee26e..8f57157 100644 --- a/go.mod +++ b/go.mod @@ -7,13 +7,13 @@ require ( github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 github.com/google/uuid v1.3.0 - github.com/gorilla/schema v1.2.0 github.com/gorilla/securecookie v1.1.1 github.com/jeremija/gosubmit v0.2.7 github.com/muhlemmer/gu v0.3.1 github.com/rs/cors v1.8.3 github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.2 + github.com/zitadel/schema v1.3.0 golang.org/x/oauth2 v0.6.0 golang.org/x/text v0.8.0 gopkg.in/square/go-jose.v2 v2.6.0 diff --git a/go.sum b/go.sum index a5ba642..b1757d4 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,6 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= -github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc= @@ -49,6 +47,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= +github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index 167f8b7..86ee1e0 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/gorilla/schema" + "github.com/zitadel/schema" "golang.org/x/text/language" "gopkg.in/square/go-jose.v2" ) diff --git a/pkg/oidc/types_test.go b/pkg/oidc/types_test.go index 64f07f1..4bf6e55 100644 --- a/pkg/oidc/types_test.go +++ b/pkg/oidc/types_test.go @@ -9,9 +9,9 @@ import ( "testing" "time" - "github.com/gorilla/schema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/zitadel/schema" "golang.org/x/text/language" ) diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index 3179e25..4e80179 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -9,7 +9,6 @@ import ( "reflect" "testing" - "github.com/gorilla/schema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" tu "github.com/zitadel/oidc/v3/internal/testutil" @@ -17,6 +16,7 @@ import ( "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/oidc/v3/pkg/op/mock" + "github.com/zitadel/schema" ) // diff --git a/pkg/op/client_test.go b/pkg/op/client_test.go index bb17192..0321f88 100644 --- a/pkg/op/client_test.go +++ b/pkg/op/client_test.go @@ -11,13 +11,13 @@ import ( "testing" "github.com/golang/mock/gomock" - "github.com/gorilla/schema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/oidc/v3/pkg/op/mock" + "github.com/zitadel/schema" ) type testClientJWTProfile struct{} diff --git a/pkg/op/mock/authorizer.mock.impl.go b/pkg/op/mock/authorizer.mock.impl.go index 409683a..4d66a92 100644 --- a/pkg/op/mock/authorizer.mock.impl.go +++ b/pkg/op/mock/authorizer.mock.impl.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/golang/mock/gomock" - "github.com/gorilla/schema" + "github.com/zitadel/schema" "gopkg.in/square/go-jose.v2" "github.com/zitadel/oidc/v3/pkg/oidc" diff --git a/pkg/op/op.go b/pkg/op/op.go index 9ed5662..1cdb3bc 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -7,8 +7,8 @@ import ( "time" "github.com/go-chi/chi" - "github.com/gorilla/schema" "github.com/rs/cors" + "github.com/zitadel/schema" "golang.org/x/text/language" "gopkg.in/square/go-jose.v2" From b7d18bfd0284b42a7dfc86dfcbf283cab19fbbd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 28 Mar 2023 14:58:57 +0300 Subject: [PATCH 211/502] chore: document non-standard glob client (#328) * op: correct typo rename checkURIAginstRedirects to checkURIAgainstRedirects * chore: document standard deviation when using globs add example on how to toggle the underlying client implementation based on DevMode. --------- Co-authored-by: David Sharnoff --- example/server/storage/client.go | 35 ++++++++++++++++++++++--------- example/server/storage/storage.go | 2 +- pkg/op/auth_request.go | 10 ++++----- pkg/op/client.go | 6 ++++++ 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/example/server/storage/client.go b/example/server/storage/client.go index b850053..b8b9960 100644 --- a/example/server/storage/client.go +++ b/example/server/storage/client.go @@ -32,6 +32,8 @@ type Client struct { devMode bool idTokenUserinfoClaimsAssertion bool clockSkew time.Duration + postLogoutRedirectURIGlobs []string + redirectURIGlobs []string } // GetID must return the client_id @@ -44,21 +46,11 @@ func (c *Client) RedirectURIs() []string { return c.redirectURIs } -// RedirectURIGlobs provide wildcarding for additional valid redirects -func (c *Client) RedirectURIGlobs() []string { - return nil -} - // PostLogoutRedirectURIs must return the registered post_logout_redirect_uris for sign-outs func (c *Client) PostLogoutRedirectURIs() []string { return []string{} } -// PostLogoutRedirectURIGlobs provide extra wildcarding for additional valid redirects -func (c *Client) PostLogoutRedirectURIGlobs() []string { - return nil -} - // ApplicationType must return the type of the client (app, native, user agent) func (c *Client) ApplicationType() op.ApplicationType { return c.applicationType @@ -200,3 +192,26 @@ func WebClient(id, secret string, redirectURIs ...string) *Client { clockSkew: 0, } } + +type hasRedirectGlobs struct { + *Client +} + +// RedirectURIGlobs provide wildcarding for additional valid redirects +func (c hasRedirectGlobs) RedirectURIGlobs() []string { + return c.redirectURIGlobs +} + +// PostLogoutRedirectURIGlobs provide extra wildcarding for additional valid redirects +func (c hasRedirectGlobs) PostLogoutRedirectURIGlobs() []string { + return c.postLogoutRedirectURIGlobs +} + +// RedirectGlobsClient wraps the client in a op.HasRedirectGlobs +// only if DevMode is enabled. +func RedirectGlobsClient(client *Client) op.Client { + if client.devMode { + return hasRedirectGlobs{client} + } + return client +} diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index acac571..a4c4f46 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -418,7 +418,7 @@ func (s *Storage) GetClientByClientID(ctx context.Context, clientID string) (op. if !ok { return nil, fmt.Errorf("client not found") } - return client, nil + return RedirectGlobsClient(client), nil } // AuthorizeClientIDSecret implements the op.Storage interface diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index b312098..1f9fc45 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -274,9 +274,9 @@ func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) { return scopes, nil } -// checkURIAginstRedirects just checks aginst the valid redirect URIs and ignores +// checkURIAgainstRedirects just checks aginst the valid redirect URIs and ignores // other factors. -func checkURIAginstRedirects(client Client, uri string) error { +func checkURIAgainstRedirects(client Client, uri string) error { if str.Contains(client.RedirectURIs(), uri) { return nil } @@ -303,12 +303,12 @@ func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.Res "Please ensure it is added to the request. If you have any questions, you may contact the administrator of the application.") } if strings.HasPrefix(uri, "https://") { - return checkURIAginstRedirects(client, uri) + return checkURIAgainstRedirects(client, uri) } if client.ApplicationType() == ApplicationTypeNative { return validateAuthReqRedirectURINative(client, uri, responseType) } - if err := checkURIAginstRedirects(client, uri); err != nil { + if err := checkURIAgainstRedirects(client, uri); err != nil { return err } if strings.HasPrefix(uri, "http://") { @@ -329,7 +329,7 @@ func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.Res func validateAuthReqRedirectURINative(client Client, uri string, responseType oidc.ResponseType) error { parsedURL, isLoopback := HTTPLoopbackOrLocalhost(uri) isCustomSchema := !strings.HasPrefix(uri, "http://") - if err := checkURIAginstRedirects(client, uri); err == nil { + if err := checkURIAgainstRedirects(client, uri); err == nil { if client.DevMode() { return nil } diff --git a/pkg/op/client.go b/pkg/op/client.go index af4724a..9da44a7 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -56,6 +56,12 @@ type Client interface { // interpretation. Redirect URIs that match either the non-glob version or the // glob version will be accepted. Glob URIs are only partially supported for native // clients: "http://" is not allowed except for loopback or in dev mode. +// +// Note that globbing / wildcards are not permitted by the OIDC +// standard and implementing this interface can have security implications. +// It is advised to only return a client of this type in rare cases, +// such as DevMode for the client being enabled. +// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest type HasRedirectGlobs interface { RedirectURIGlobs() []string PostLogoutRedirectURIGlobs() []string From 1a2db3683f135ced14022705373d21789d6ec9f2 Mon Sep 17 00:00:00 2001 From: Thomas Hipp Date: Wed, 29 Mar 2023 09:51:10 +0200 Subject: [PATCH 212/502] fix: Only set GrantType once (#353) This fixes an issue where, when using the device authorization flow, the grant type would be set twice. Some OPs don't accept this, and fail when polling. With this fix the grant type is only set once, which will make some OPs happy again. Fixes #352 --- pkg/client/rp/device.go | 1 - pkg/oidc/token_request.go | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/client/rp/device.go b/pkg/client/rp/device.go index 73b67ca..a397f14 100644 --- a/pkg/client/rp/device.go +++ b/pkg/client/rp/device.go @@ -12,7 +12,6 @@ import ( func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.ClientCredentialsRequest, error) { confg := rp.OAuthConfig() req := &oidc.ClientCredentialsRequest{ - GrantType: oidc.GrantTypeDeviceCode, Scope: scopes, ClientID: confg.ClientID, ClientSecret: confg.ClientSecret, diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index 6b6945a..5c5cf20 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -241,7 +241,7 @@ type TokenExchangeRequest struct { } type ClientCredentialsRequest struct { - GrantType GrantType `schema:"grant_type"` + GrantType GrantType `schema:"grant_type,omitempty"` Scope SpaceDelimitedArray `schema:"scope"` ClientID string `schema:"client_id"` ClientSecret string `schema:"client_secret"` From c778e8329c2b694a1e9231c3babd4489e6a4667e Mon Sep 17 00:00:00 2001 From: Thomas Hipp Date: Mon, 3 Apr 2023 14:40:29 +0200 Subject: [PATCH 213/502] feat: Allow modifying request to device authorization endpoint (#356) * feat: Allow modifying request to device authorization endpoint This change enables the caller to set URL parameters when calling the device authorization endpoint. Fixes #354 * Update device authorization example --- example/client/device/device.go | 2 +- pkg/client/client.go | 4 ++-- pkg/client/rp/device.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/example/client/device/device.go b/example/client/device/device.go index c186b34..bea6134 100644 --- a/example/client/device/device.go +++ b/example/client/device/device.go @@ -45,7 +45,7 @@ func main() { } logrus.Info("starting device authorization flow") - resp, err := rp.DeviceAuthorization(ctx, scopes, provider) + resp, err := rp.DeviceAuthorization(ctx, scopes, provider, nil) if err != nil { logrus.Fatal(err) } diff --git a/pkg/client/client.go b/pkg/client/client.go index b9580ff..37c7ec2 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -186,8 +186,8 @@ type DeviceAuthorizationCaller interface { HttpClient() *http.Client } -func CallDeviceAuthorizationEndpoint(ctx context.Context, request *oidc.ClientCredentialsRequest, caller DeviceAuthorizationCaller) (*oidc.DeviceAuthorizationResponse, error) { - req, err := httphelper.FormRequest(ctx, caller.GetDeviceAuthorizationEndpoint(), request, Encoder, nil) +func CallDeviceAuthorizationEndpoint(ctx context.Context, request *oidc.ClientCredentialsRequest, caller DeviceAuthorizationCaller, authFn any) (*oidc.DeviceAuthorizationResponse, error) { + req, err := httphelper.FormRequest(ctx, caller.GetDeviceAuthorizationEndpoint(), request, Encoder, authFn) if err != nil { return nil, err } diff --git a/pkg/client/rp/device.go b/pkg/client/rp/device.go index b2c5be6..788e23e 100644 --- a/pkg/client/rp/device.go +++ b/pkg/client/rp/device.go @@ -33,13 +33,13 @@ func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc. // DeviceAuthorization starts a new Device Authorization flow as defined // in RFC 8628, section 3.1 and 3.2: // https://www.rfc-editor.org/rfc/rfc8628#section-3.1 -func DeviceAuthorization(ctx context.Context, scopes []string, rp RelyingParty) (*oidc.DeviceAuthorizationResponse, error) { +func DeviceAuthorization(ctx context.Context, scopes []string, rp RelyingParty, authFn any) (*oidc.DeviceAuthorizationResponse, error) { req, err := newDeviceClientCredentialsRequest(scopes, rp) if err != nil { return nil, err } - return client.CallDeviceAuthorizationEndpoint(ctx, req, rp) + return client.CallDeviceAuthorizationEndpoint(ctx, req, rp, authFn) } // DeviceAccessToken attempts to obtain tokens from a Device Authorization, From 211b17589ee25ede48a30cba51ff3ad3b10149ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Apr 2023 07:36:29 +0200 Subject: [PATCH 214/502] chore(deps): bump actions/add-to-project from 0.4.1 to 0.5.0 (#357) Bumps [actions/add-to-project](https://github.com/actions/add-to-project) from 0.4.1 to 0.5.0. - [Release notes](https://github.com/actions/add-to-project/releases) - [Commits](https://github.com/actions/add-to-project/compare/v0.4.1...v0.5.0) --- updated-dependencies: - dependency-name: actions/add-to-project dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/issue.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 8671820..362443d 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -10,7 +10,7 @@ jobs: name: Add issue to project runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@v0.4.1 + - uses: actions/add-to-project@v0.5.0 with: # You can target a repository in a different organization # to the issue From dc2bdc6202866510a1e2caaaaa2200ba156e258a Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Tue, 4 Apr 2023 12:48:18 +0200 Subject: [PATCH 215/502] fix: improve error handling when getting ClientIDFromRequest (#359) --- pkg/op/client.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/pkg/op/client.go b/pkg/op/client.go index 9da44a7..04f8c0c 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -156,16 +156,25 @@ func ClientIDFromRequest(r *http.Request, p ClientProvider) (clientID string, au } JWTProfile, ok := p.(ClientJWTProfile) - if ok { + if ok && data.ClientAssertion != "" { + // if JWTProfile is supported and client sent an assertion, check it and use it as response + // regardless if it succeeded or failed clientID, err = ClientJWTAuth(r.Context(), data.ClientAssertionParams, JWTProfile) + return clientID, err == nil, err } - if !ok || errors.Is(err, ErrNoClientCredentials) { - clientID, err = ClientBasicAuth(r, p.Storage()) - } + // try basic auth + clientID, err = ClientBasicAuth(r, p.Storage()) + // if that succeeded, use it if err == nil { return clientID, true, nil } + // if the client did not send a Basic Auth Header, ignore the `ErrNoClientCredentials` + // but return other errors immediately + if err != nil && !errors.Is(err, ErrNoClientCredentials) { + return "", false, err + } + // if the client did not authenticate (public clients) it must at least send a client_id if data.ClientID == "" { return "", false, oidc.ErrInvalidClient().WithParent(ErrMissingClientID) } From c72aa8f9a1e799e3165de2ba10e8cd3ca5b9d113 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Tue, 4 Apr 2023 13:45:30 +0200 Subject: [PATCH 216/502] fix: use Form instead of PostForm in ClientIDFromRequest (#360) --- pkg/op/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/op/client.go b/pkg/op/client.go index 04f8c0c..32a3dd1 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -151,7 +151,7 @@ func ClientIDFromRequest(r *http.Request, p ClientProvider) (clientID string, au } data := new(clientData) - if err = p.Decoder().Decode(data, r.PostForm); err != nil { + if err = p.Decoder().Decode(data, r.Form); err != nil { return "", false, err } From 057538d55546bb7aa95dcb5fb2d7b2d51cc321ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 5 Apr 2023 10:02:37 +0200 Subject: [PATCH 217/502] fix: resolve nil pointer panic in Authorize (#358) When ParseAuthorizeRequest received an invalid URL, for example containing a semi-colon `;`, AuthRequestError used to panic. This was because a typed nil was passed as a interface argument. The nil check inside AuthRequestError always resulted in false, allowing access through the nil pointer. Fixes #315 --- pkg/op/auth_request.go | 2 +- pkg/op/auth_request_test.go | 83 +++++++++++++------------------------ 2 files changed, 30 insertions(+), 55 deletions(-) diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 1f9fc45..c264605 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -68,7 +68,7 @@ func authorizeCallbackHandler(authorizer Authorizer) func(http.ResponseWriter, * func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { authReq, err := ParseAuthorizeRequest(r, authorizer.Decoder()) if err != nil { - AuthRequestError(w, r, authReq, err, authorizer.Encoder()) + AuthRequestError(w, r, nil, err, authorizer.Encoder()) return } ctx := r.Context() diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index 7a9701b..2bba4e7 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -9,6 +9,7 @@ import ( "reflect" "testing" + "github.com/golang/mock/gomock" "github.com/gorilla/schema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -19,60 +20,34 @@ import ( "github.com/zitadel/oidc/v2/pkg/op/mock" ) -// -// TOOD: tests will be implemented in branch for service accounts -// func TestAuthorize(t *testing.T) { -// // testCallback := func(t *testing.T, clienID string) callbackHandler { -// // return func(authReq *oidc.AuthRequest, client oidc.Client, w http.ResponseWriter, r *http.Request) { -// // // require.Equal(t, clientID, client.) -// // } -// // } -// // testErr := func(t *testing.T, expected error) errorHandler { -// // return func(w http.ResponseWriter, r *http.Request, authReq *oidc.AuthRequest, err error) { -// // require.Equal(t, expected, err) -// // } -// // } -// type args struct { -// w http.ResponseWriter -// r *http.Request -// authorizer op.Authorizer -// } -// tests := []struct { -// name string -// args args -// }{ -// { -// "parsing fails", -// args{ -// httptest.NewRecorder(), -// &http.Request{Method: "POST", Body: nil}, -// mock.NewAuthorizerExpectValid(t, true), -// // testCallback(t, ""), -// // testErr(t, ErrInvalidRequest("cannot parse form")), -// }, -// }, -// { -// "decoding fails", -// args{ -// httptest.NewRecorder(), -// func() *http.Request { -// r := httptest.NewRequest("POST", "/authorize", strings.NewReader("client_id=foo")) -// r.Header.Set("Content-Type", "application/x-www-form-urlencoded") -// return r -// }(), -// mock.NewAuthorizerExpectValid(t, true), -// // testCallback(t, ""), -// // testErr(t, ErrInvalidRequest("cannot parse auth request")), -// }, -// }, -// // {"decoding fails", args{httptest.NewRecorder(), &http.Request{}, mock.NewAuthorizerExpectValid(t), nil, testErr(t, nil)}}, -// } -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// op.Authorize(tt.args.w, tt.args.r, tt.args.authorizer) -// }) -// } -//} +func TestAuthorize(t *testing.T) { + tests := []struct { + name string + req *http.Request + expect func(a *mock.MockAuthorizerMockRecorder) + }{ + { + name: "parse error", // used to panic, see issue #315 + req: httptest.NewRequest(http.MethodPost, "/?;", nil), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := httptest.NewRecorder() + authorizer := mock.NewMockAuthorizer(gomock.NewController(t)) + + expect := authorizer.EXPECT() + expect.Decoder().Return(schema.NewDecoder()) + expect.Encoder().Return(schema.NewEncoder()) + + if tt.expect != nil { + tt.expect(expect) + } + + op.Authorize(w, tt.req, authorizer) + }) + } +} func TestParseAuthorizeRequest(t *testing.T) { type args struct { From 54c87ada6f61f9a4fa7d07b7274cc6e9d3c3fdae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 10:35:15 +0300 Subject: [PATCH 218/502] chore(deps): bump golang.org/x/text from 0.8.0 to 0.9.0 (#361) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.8.0 to 0.9.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.8.0...v0.9.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index adb638e..09c1f69 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.2 golang.org/x/oauth2 v0.6.0 - golang.org/x/text v0.8.0 + golang.org/x/text v0.9.0 gopkg.in/square/go-jose.v2 v2.6.0 ) diff --git a/go.sum b/go.sum index 4259674..e8e5db7 100644 --- a/go.sum +++ b/go.sum @@ -79,8 +79,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= From 97bc09583d7028ec1c4f75396ed77062a04f897a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 10:37:08 +0300 Subject: [PATCH 219/502] chore(deps): bump golang.org/x/oauth2 from 0.6.0 to 0.7.0 (#362) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.6.0 to 0.7.0. - [Release notes](https://github.com/golang/oauth2/releases) - [Commits](https://github.com/golang/oauth2/compare/v0.6.0...v0.7.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 09c1f69..f4e77f6 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/rs/cors v1.8.3 github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.2 - golang.org/x/oauth2 v0.6.0 + golang.org/x/oauth2 v0.7.0 golang.org/x/text v0.9.0 gopkg.in/square/go-jose.v2 v2.6.0 ) @@ -26,8 +26,8 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/crypto v0.7.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/sys v0.6.0 // indirect + golang.org/x/net v0.9.0 // indirect + golang.org/x/sys v0.7.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.29.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index e8e5db7..8d3f108 100644 --- a/go.sum +++ b/go.sum @@ -60,11 +60,11 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= -golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -73,8 +73,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= From 44f840357475b1923c2dd3b2ef5240da189ea345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 11 Apr 2023 21:29:17 +0300 Subject: [PATCH 220/502] feat: get issuer from context for device auth (#363) * feat: get issuer from context for device auth * use distinct UserFormURL and UserFormPath - Properly deprecate UserFormURL and default to old behaviour, to prevent breaking change. - Refactor unit tests to test both cases. * update example --- example/server/exampleop/op.go | 2 +- pkg/op/device.go | 27 ++++++++++++-- pkg/op/device_test.go | 64 +++++++++++++++++++++++++--------- pkg/op/op_test.go | 29 ++++++++------- 4 files changed, 90 insertions(+), 32 deletions(-) diff --git a/example/server/exampleop/op.go b/example/server/exampleop/op.go index 5604483..1dc8bd1 100644 --- a/example/server/exampleop/op.go +++ b/example/server/exampleop/op.go @@ -107,7 +107,7 @@ func newOP(storage op.Storage, issuer string, key [32]byte) (op.OpenIDProvider, DeviceAuthorization: op.DeviceAuthorizationConfig{ Lifetime: 5 * time.Minute, PollInterval: 5 * time.Second, - UserFormURL: issuer + "device", + UserFormPath: "/device", UserCode: op.UserCodeBase20, }, } diff --git a/pkg/op/device.go b/pkg/op/device.go index 04c06f2..f7691ca 100644 --- a/pkg/op/device.go +++ b/pkg/op/device.go @@ -8,6 +8,7 @@ import ( "fmt" "math/big" "net/http" + "net/url" "strings" "time" @@ -18,7 +19,14 @@ import ( type DeviceAuthorizationConfig struct { Lifetime time.Duration PollInterval time.Duration - UserFormURL string // the URL where the user must go to authorize the device + + // UserFormURL is the complete URL where the user must go to authorize the device. + // Deprecated: use UserFormPath instead. + UserFormURL string + + // UserFormPath is the path where the user must go to authorize the device. + // The hostname for the URL is taken from the request by IssuerFromContext. + UserFormPath string UserCode UserCodeConfig } @@ -82,15 +90,28 @@ func DeviceAuthorization(w http.ResponseWriter, r *http.Request, o OpenIDProvide return err } + var verification *url.URL + if config.UserFormURL != "" { + if verification, err = url.Parse(config.UserFormURL); err != nil { + return oidc.ErrServerError().WithParent(err).WithDescription("invalid URL for device user form") + } + } else { + if verification, err = url.Parse(IssuerFromContext(r.Context())); err != nil { + return oidc.ErrServerError().WithParent(err).WithDescription("invalid URL for issuer") + } + verification.Path = config.UserFormPath + } + response := &oidc.DeviceAuthorizationResponse{ DeviceCode: deviceCode, UserCode: userCode, - VerificationURI: config.UserFormURL, + VerificationURI: verification.String(), ExpiresIn: int(config.Lifetime / time.Second), Interval: int(config.PollInterval / time.Second), } - response.VerificationURIComplete = fmt.Sprintf("%s?user_code=%s", config.UserFormURL, userCode) + verification.RawQuery = "user_code=" + userCode + response.VerificationURIComplete = verification.String() httphelper.MarshalJSON(w, response) return nil diff --git a/pkg/op/device_test.go b/pkg/op/device_test.go index 69ba102..cf94c3f 100644 --- a/pkg/op/device_test.go +++ b/pkg/op/device_test.go @@ -13,6 +13,7 @@ import ( "testing" "time" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zitadel/oidc/v2/pkg/oidc" @@ -20,29 +21,60 @@ import ( ) func Test_deviceAuthorizationHandler(t *testing.T) { - req := &oidc.DeviceAuthorizationRequest{ - Scopes: []string{"foo", "bar"}, - ClientID: "web", + type conf struct { + UserFormURL string + UserFormPath string } - values := make(url.Values) - testProvider.Encoder().Encode(req, values) - body := strings.NewReader(values.Encode()) + tests := []struct { + name string + conf conf + }{ + { + name: "UserFormURL", + conf: conf{ + UserFormURL: "https://localhost:9998/device", + }, + }, + { + name: "UserFormPath", + conf: conf{ + UserFormPath: "/device", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf := gu.PtrCopy(testConfig) + conf.DeviceAuthorization.UserFormURL = tt.conf.UserFormURL + conf.DeviceAuthorization.UserFormPath = tt.conf.UserFormPath + provider := newTestProvider(conf) - r := httptest.NewRequest(http.MethodPost, "/", body) - r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req := &oidc.DeviceAuthorizationRequest{ + Scopes: []string{"foo", "bar"}, + ClientID: "web", + } + values := make(url.Values) + testProvider.Encoder().Encode(req, values) + body := strings.NewReader(values.Encode()) - w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "/", body) + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + r = r.WithContext(op.ContextWithIssuer(r.Context(), testIssuer)) - runWithRandReader(mr.New(mr.NewSource(1)), func() { - op.DeviceAuthorizationHandler(testProvider)(w, r) - }) + w := httptest.NewRecorder() - result := w.Result() + runWithRandReader(mr.New(mr.NewSource(1)), func() { + op.DeviceAuthorizationHandler(provider)(w, r) + }) - assert.Less(t, result.StatusCode, 300) + result := w.Result() - got, _ := io.ReadAll(result.Body) - assert.JSONEq(t, `{"device_code":"Uv38ByGCZU8WP18PmmIdcg", "expires_in":300, "interval":5, "user_code":"JKRV-FRGK", "verification_uri":"https://localhost:9998/device", "verification_uri_complete":"https://localhost:9998/device?user_code=JKRV-FRGK"}`, string(got)) + assert.Less(t, result.StatusCode, 300) + + got, _ := io.ReadAll(result.Body) + assert.JSONEq(t, `{"device_code":"Uv38ByGCZU8WP18PmmIdcg", "expires_in":300, "interval":5, "user_code":"JKRV-FRGK", "verification_uri":"https://localhost:9998/device", "verification_uri_complete":"https://localhost:9998/device?user_code=JKRV-FRGK"}`, string(got)) + }) + } } func TestParseDeviceCodeRequest(t *testing.T) { diff --git a/pkg/op/op_test.go b/pkg/op/op_test.go index ba3570b..3e6377f 100644 --- a/pkg/op/op_test.go +++ b/pkg/op/op_test.go @@ -20,15 +20,9 @@ import ( "golang.org/x/text/language" ) -var testProvider op.OpenIDProvider - -const ( - testIssuer = "https://localhost:9998/" - pathLoggedOut = "/logged-out" -) - -func init() { - config := &op.Config{ +var ( + testProvider op.OpenIDProvider + testConfig = &op.Config{ CryptoKey: sha256.Sum256([]byte("test")), DefaultLogoutRedirectURI: pathLoggedOut, CodeMethodS256: true, @@ -40,24 +34,35 @@ func init() { DeviceAuthorization: op.DeviceAuthorizationConfig{ Lifetime: 5 * time.Minute, PollInterval: 5 * time.Second, - UserFormURL: testIssuer + "device", + UserFormPath: "/device", UserCode: op.UserCodeBase20, }, } +) +const ( + testIssuer = "https://localhost:9998/" + pathLoggedOut = "/logged-out" +) + +func init() { storage.RegisterClients( storage.NativeClient("native"), storage.WebClient("web", "secret", "https://example.com"), storage.WebClient("api", "secret"), ) - var err error - testProvider, err = op.NewOpenIDProvider(testIssuer, config, + testProvider = newTestProvider(testConfig) +} + +func newTestProvider(config *op.Config) op.OpenIDProvider { + provider, err := op.NewOpenIDProvider(testIssuer, config, storage.NewStorage(storage.NewUserStore(testIssuer)), op.WithAllowInsecure(), ) if err != nil { panic(err) } + return provider } type routesTestStorage interface { From 8730a1685e185d681392e17f5f402e60dce8b152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Thu, 13 Apr 2023 12:25:49 +0300 Subject: [PATCH 221/502] feat: custom endpoint for device authorization (#368) --- pkg/op/op.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/op/op.go b/pkg/op/op.go index ecb753e..fc5262a 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -476,6 +476,16 @@ func WithCustomKeysEndpoint(endpoint Endpoint) Option { } } +func WithCustomDeviceAuthorizationEndpoint(endpoint Endpoint) Option { + return func(o *Provider) error { + if err := endpoint.Validate(); err != nil { + return err + } + o.endpoints.DeviceAuthorization = endpoint + return nil + } +} + func WithCustomEndpoints(auth, token, userInfo, revocation, endSession, keys Endpoint) Option { return func(o *Provider) error { o.endpoints.Authorization = auth From 312c2a07e21bd1c3e862150da1f45cbddc6ba45c Mon Sep 17 00:00:00 2001 From: Thomas Hipp Date: Thu, 13 Apr 2023 15:04:58 +0200 Subject: [PATCH 222/502] fix: Only set GrantType once (#353) (#367) This fixes an issue where, when using the device authorization flow, the grant type would be set twice. Some OPs don't accept this, and fail when polling. With this fix the grant type is only set once, which will make some OPs happy again. Fixes #352 --- pkg/client/rp/device.go | 1 - pkg/oidc/token_request.go | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/client/rp/device.go b/pkg/client/rp/device.go index 788e23e..390c8cf 100644 --- a/pkg/client/rp/device.go +++ b/pkg/client/rp/device.go @@ -12,7 +12,6 @@ import ( func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.ClientCredentialsRequest, error) { confg := rp.OAuthConfig() req := &oidc.ClientCredentialsRequest{ - GrantType: oidc.GrantTypeDeviceCode, Scope: scopes, ClientID: confg.ClientID, ClientSecret: confg.ClientSecret, diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index 6b6945a..5c5cf20 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -241,7 +241,7 @@ type TokenExchangeRequest struct { } type ClientCredentialsRequest struct { - GrantType GrantType `schema:"grant_type"` + GrantType GrantType `schema:"grant_type,omitempty"` Scope SpaceDelimitedArray `schema:"scope"` ClientID string `schema:"client_id"` ClientSecret string `schema:"client_secret"` From f0d46593e0aa984bb1a63fc93cb79fc73b7ab59b Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Thu, 13 Apr 2023 06:37:50 -0700 Subject: [PATCH 223/502] feat: rp.RefreshAccessToken() now may provide an updated IDToken (#365) --- pkg/client/client.go | 10 ++++++++-- pkg/client/integration_test.go | 2 +- pkg/client/profile/jwt_profile.go | 2 +- pkg/client/rp/relying_party.go | 4 ++++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index 9eda973..f6a407b 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -61,12 +61,18 @@ func callTokenEndpoint(request interface{}, authFn interface{}, caller TokenEndp if err := httphelper.HttpRequest(caller.HttpClient(), req, &tokenRes); err != nil { return nil, err } - return &oauth2.Token{ + token := &oauth2.Token{ AccessToken: tokenRes.AccessToken, TokenType: tokenRes.TokenType, RefreshToken: tokenRes.RefreshToken, Expiry: time.Now().UTC().Add(time.Duration(tokenRes.ExpiresIn) * time.Second), - }, nil + } + if tokenRes.IDToken != "" { + token = token.WithExtra(map[string]any{ + "id_token": tokenRes.IDToken, + }) + } + return token, nil } type EndSessionCaller interface { diff --git a/pkg/client/integration_test.go b/pkg/client/integration_test.go index e19a720..40e1bee 100644 --- a/pkg/client/integration_test.go +++ b/pkg/client/integration_test.go @@ -53,6 +53,7 @@ func TestRelyingPartySession(t *testing.T) { t.Logf("new token type %s", newTokens.TokenType) t.Logf("new expiry %s", newTokens.Expiry.Format(time.RFC3339)) require.NotEmpty(t, newTokens.AccessToken, "new accessToken") + assert.NotEmpty(t, newTokens.Extra("id_token"), "new idToken") t.Log("------ end session (logout) ------") @@ -141,7 +142,6 @@ func TestResourceServerTokenExchange(t *testing.T) { require.Error(t, err, "refresh token") assert.Contains(t, err.Error(), "subject_token is invalid") require.Nil(t, tokenExchangeResponse, "token exchange response") - } func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID, clientSecret string) (provider rp.RelyingParty, accessToken, refreshToken, idToken string) { diff --git a/pkg/client/profile/jwt_profile.go b/pkg/client/profile/jwt_profile.go index a934f7d..a220dc5 100644 --- a/pkg/client/profile/jwt_profile.go +++ b/pkg/client/profile/jwt_profile.go @@ -13,7 +13,7 @@ import ( // jwtProfileTokenSource implement the oauth2.TokenSource // it will request a token using the OAuth2 JWT Profile Grant -// therefore sending an `assertion` by singing a JWT with the provided private key +// therefore sending an `assertion` by signing a JWT with the provided private key type jwtProfileTokenSource struct { clientID string audience []string diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index ede7453..7127020 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -620,6 +620,10 @@ type RefreshTokenRequest struct { GrantType oidc.GrantType `schema:"grant_type"` } +// RefreshAccessToken performs a token refresh. If it doesn't error, it will always +// provide a new AccessToken. It may provide a new RefreshToken, and if it does, then +// the old one should be considered invalid. It may also provide a new IDToken. The +// new IDToken can be retrieved with token.Extra("id_token"). func RefreshAccessToken(rp RelyingParty, refreshToken, clientAssertion, clientAssertionType string) (*oauth2.Token, error) { request := RefreshTokenRequest{ RefreshToken: refreshToken, From 2c7ca3a30579084555028dc4c0c7f5688de454d2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Apr 2023 15:32:02 +0300 Subject: [PATCH 224/502] chore(deps): bump github.com/rs/cors from 1.8.3 to 1.9.0 (#369) Bumps [github.com/rs/cors](https://github.com/rs/cors) from 1.8.3 to 1.9.0. - [Release notes](https://github.com/rs/cors/releases) - [Commits](https://github.com/rs/cors/compare/v1.8.3...v1.9.0) --- updated-dependencies: - dependency-name: github.com/rs/cors dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f4e77f6..5f96c72 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/gorilla/securecookie v1.1.1 github.com/jeremija/gosubmit v0.2.7 github.com/muhlemmer/gu v0.3.1 - github.com/rs/cors v1.8.3 + github.com/rs/cors v1.9.0 github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.2 golang.org/x/oauth2 v0.7.0 diff --git a/go.sum b/go.sum index 8d3f108..df4d60c 100644 --- a/go.sum +++ b/go.sum @@ -36,8 +36,8 @@ github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo= -github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= +github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From 7aa96feb6a8454782a53b22d47d9dba3044c2aa1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Apr 2023 12:15:21 +0300 Subject: [PATCH 225/502] chore(deps): bump codecov/codecov-action from 3.1.1 to 3.1.2 (#373) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3.1.1 to 3.1.2. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v3.1.1...v3.1.2) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 78c0f79..7483b2f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v3.1.1 + - uses: codecov/codecov-action@v3.1.2 with: file: ./profile.cov name: codecov-go From d3359d7c72c40c5668681ab59d1529b95e1a75d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Apr 2023 12:27:55 +0300 Subject: [PATCH 226/502] chore(deps): bump codecov/codecov-action from 3.1.2 to 3.1.3 (#381) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3.1.2 to 3.1.3. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v3.1.2...v3.1.3) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7483b2f..2ffd9c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v3.1.2 + - uses: codecov/codecov-action@v3.1.3 with: file: ./profile.cov name: codecov-go From 7997994be483ad30c994de6a4bcc04f4dce682b7 Mon Sep 17 00:00:00 2001 From: mffap Date: Wed, 26 Apr 2023 11:29:35 +0200 Subject: [PATCH 227/502] chore(docs): add oidc link to badge (#382) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f369a5c..e5a992f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/zitadel/oidc)](https://goreportcard.com/report/github.com/zitadel/oidc) [![codecov](https://codecov.io/gh/zitadel/oidc/branch/main/graph/badge.svg)](https://codecov.io/gh/zitadel/oidc) -![openid_certified](https://cloud.githubusercontent.com/assets/1454075/7611268/4d19de32-f97b-11e4-895b-31b2455a7ca6.png) +[![openid_certified](https://cloud.githubusercontent.com/assets/1454075/7611268/4d19de32-f97b-11e4-895b-31b2455a7ca6.png)](https://openid.net/certification/) ## What Is It From edf306219fd4b6e96eb75e82454c5d3bc26d9fb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 2 May 2023 12:31:30 +0300 Subject: [PATCH 228/502] chore(rp): add a custom claims test for VerifyIDToken (#375) --- pkg/client/rp/verifier_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pkg/client/rp/verifier_test.go b/pkg/client/rp/verifier_test.go index 7588c1f..f4e0f9d 100644 --- a/pkg/client/rp/verifier_test.go +++ b/pkg/client/rp/verifier_test.go @@ -113,6 +113,18 @@ func TestVerifyIDToken(t *testing.T) { clientID: tu.ValidClientID, tokenClaims: tu.ValidIDToken, }, + { + name: "custom claims", + clientID: tu.ValidClientID, + tokenClaims: func() (string, *oidc.IDTokenClaims) { + return tu.NewIDTokenCustom( + tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce, + tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "", + map[string]any{"some": "thing"}, + ) + }, + }, { name: "parse err", clientID: tu.ValidClientID, From 54eb8236376bb6f5f76ca161dee0bd94b36c68ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 2 May 2023 12:35:15 +0300 Subject: [PATCH 229/502] chore: update securty policy to latest versions (#380) --- SECURITY.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 934426a..d682630 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,13 +4,21 @@ At ZITADEL we are extremely grateful for security aware people that disclose vul ## Supported Versions -After the initial Release the following version support will apply +We currently support the following version of the OIDC framework: -| Version | Supported | -| ------- | ------------------ | -| 0.x.x | :x: | -| 1.x.x | :white_check_mark: | -| 2.x.x | :white_check_mark: (not released) | +| Version | Supported | Branch | Details | +| -------- | ------------------ | ----------- | ------------------------------------ | +| 0.x.x | :x: | | not maintained | +| <1.13 | :x: | | not maintained | +| 1.13.x | :lock: :warning: | [1.13.x][1] | security only, [community effort][2] | +| 2.x.x | :heavy_check_mark: | [main][3] | supported | +| 3.0.0-xx | :white_check_mark: | [next][4] | [developement branch][5] | + +[1]: https://github.com/zitadel/oidc/tree/1.13.x +[2]: https://github.com/zitadel/oidc/discussions/378 +[3]: https://github.com/zitadel/oidc/tree/main +[4]: https://github.com/zitadel/oidc/tree/next +[5]: https://github.com/zitadel/oidc/milestone/2 ## Reporting a vulnerability From e62473ba71b6a90adba5ce6fb21e2c921c70be74 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Wed, 3 May 2023 03:09:19 -0700 Subject: [PATCH 230/502] chore: improve error message when issuer is invalid (#383) --- pkg/client/rp/relying_party.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 7127020..108fa4f 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -325,7 +325,7 @@ func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { return Endpoints{}, err } if discoveryConfig.Issuer != issuer { - return Endpoints{}, oidc.ErrIssuerInvalid + return Endpoints{}, fmt.Errorf("%w: Expected: %s, got: %s", oidc.ErrIssuerInvalid, discoveryConfig.Issuer, issuer) } return GetEndpoints(discoveryConfig), nil } From e43ac6dfdfd9c9ae928e45628b9471745f327384 Mon Sep 17 00:00:00 2001 From: Giulio Ruggeri Date: Wed, 3 May 2023 12:27:28 +0200 Subject: [PATCH 231/502] fix: modify ACRValues parameter type to space separated strings (#388) Co-authored-by: Giulio Ruggeri --- pkg/oidc/authorization.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/oidc/authorization.go b/pkg/oidc/authorization.go index f620ecb..d8bf336 100644 --- a/pkg/oidc/authorization.go +++ b/pkg/oidc/authorization.go @@ -60,7 +60,7 @@ const ( ) // AuthRequest according to: -//https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest +// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest type AuthRequest struct { Scopes SpaceDelimitedArray `json:"scope" schema:"scope"` ResponseType ResponseType `json:"response_type" schema:"response_type"` @@ -77,7 +77,7 @@ type AuthRequest struct { UILocales Locales `json:"ui_locales" schema:"ui_locales"` IDTokenHint string `json:"id_token_hint" schema:"id_token_hint"` LoginHint string `json:"login_hint" schema:"login_hint"` - ACRValues []string `json:"acr_values" schema:"acr_values"` + ACRValues SpaceDelimitedArray `json:"acr_values" schema:"acr_values"` CodeChallenge string `json:"code_challenge" schema:"code_challenge"` CodeChallengeMethod CodeChallengeMethod `json:"code_challenge_method" schema:"code_challenge_method"` From 157bc6ceb02c6d99c8f772fe46b07def81e99c65 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Wed, 3 May 2023 03:56:47 -0700 Subject: [PATCH 232/502] feat: coverage prompt=none, response_mode=fragment (#385) --- example/server/exampleop/op.go | 16 +++--- example/server/storage/storage.go | 12 ++++- pkg/client/integration_test.go | 87 +++++++++++++++++++++++++++++++ pkg/client/rp/relying_party.go | 5 ++ pkg/oidc/authorization.go | 7 ++- 5 files changed, 117 insertions(+), 10 deletions(-) diff --git a/example/server/exampleop/op.go b/example/server/exampleop/op.go index 1dc8bd1..7254585 100644 --- a/example/server/exampleop/op.go +++ b/example/server/exampleop/op.go @@ -34,7 +34,7 @@ type Storage interface { // SetupServer creates an OIDC server with Issuer=http://localhost: // // Use one of the pre-made clients in storage/clients.go or register a new one. -func SetupServer(issuer string, storage Storage) *mux.Router { +func SetupServer(issuer string, storage Storage, extraOptions ...op.Option) *mux.Router { // the OpenID Provider requires a 32-byte key for (token) encryption // be sure to create a proper crypto random key and manage it securely! key := sha256.Sum256([]byte("test")) @@ -50,7 +50,7 @@ func SetupServer(issuer string, storage Storage) *mux.Router { }) // creation of the OpenIDProvider with the just created in-memory Storage - provider, err := newOP(storage, issuer, key) + provider, err := newOP(storage, issuer, key, extraOptions...) if err != nil { log.Fatal(err) } @@ -79,7 +79,7 @@ func SetupServer(issuer string, storage Storage) *mux.Router { // newOP will create an OpenID Provider for localhost on a specified port with a given encryption key // and a predefined default logout uri // it will enable all options (see descriptions) -func newOP(storage op.Storage, issuer string, key [32]byte) (op.OpenIDProvider, error) { +func newOP(storage op.Storage, issuer string, key [32]byte, extraOptions ...op.Option) (op.OpenIDProvider, error) { config := &op.Config{ CryptoKey: key, @@ -112,10 +112,12 @@ func newOP(storage op.Storage, issuer string, key [32]byte) (op.OpenIDProvider, }, } handler, err := op.NewOpenIDProvider(issuer, config, storage, - //we must explicitly allow the use of the http issuer - op.WithAllowInsecure(), - // as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth - op.WithCustomAuthEndpoint(op.NewEndpoint("auth")), + append([]op.Option{ + // we must explicitly allow the use of the http issuer + op.WithAllowInsecure(), + // as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth + op.WithCustomAuthEndpoint(op.NewEndpoint("auth")), + }, extraOptions...)..., ) if err != nil { return nil, err diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index a4c4f46..406300b 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -28,8 +28,10 @@ var serviceKey1 = &rsa.PublicKey{ E: 65537, } -var _ op.Storage = &Storage{} -var _ op.ClientCredentialsStorage = &Storage{} +var ( + _ op.Storage = &Storage{} + _ op.ClientCredentialsStorage = &Storage{} +) // storage implements the op.Storage interface // typically you would implement this as a layer on top of your database @@ -167,6 +169,12 @@ func (s *Storage) CreateAuthRequest(ctx context.Context, authReq *oidc.AuthReque s.lock.Lock() defer s.lock.Unlock() + if len(authReq.Prompt) == 1 && authReq.Prompt[0] == "none" { + // With prompt=none, there is no way for the user to log in + // so return error right away. + return nil, oidc.ErrLoginRequired() + } + // typically, you'll fill your storage / storage model with the information of the passed object request := authRequestToInternal(authReq, userID) diff --git a/pkg/client/integration_test.go b/pkg/client/integration_test.go index 40e1bee..ea7225d 100644 --- a/pkg/client/integration_test.go +++ b/pkg/client/integration_test.go @@ -25,6 +25,7 @@ import ( "github.com/zitadel/oidc/v2/pkg/client/tokenexchange" httphelper "github.com/zitadel/oidc/v2/pkg/http" "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" ) func TestRelyingPartySession(t *testing.T) { @@ -280,6 +281,92 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID, return provider, accessToken, refreshToken, idToken } +func TestErrorFromPromptNone(t *testing.T) { + jar, err := cookiejar.New(nil) + require.NoError(t, err, "create cookie jar") + httpClient := &http.Client{ + Timeout: time.Second * 5, + CheckRedirect: func(_ *http.Request, _ []*http.Request) error { + return http.ErrUseLastResponse + }, + Jar: jar, + } + + t.Log("------- start example OP ------") + targetURL := "http://local-site" + exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL)) + var dh deferredHandler + opServer := httptest.NewServer(&dh) + defer opServer.Close() + t.Logf("auth server at %s", opServer.URL) + dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, op.WithHttpInterceptors( + func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Logf("request to %s", r.URL) + next.ServeHTTP(w, r) + }) + }, + )) + seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano())) + clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25) + clientSecret := "secret" + client := storage.WebClient(clientID, clientSecret, targetURL) + storage.RegisterClients(client) + + t.Log("------- create RP ------") + key := []byte("test1234test1234") + cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure()) + provider, err := rp.NewRelyingPartyOIDC( + opServer.URL, + clientID, + clientSecret, + targetURL, + []string{"openid", "email", "profile", "offline_access"}, + rp.WithPKCE(cookieHandler), + rp.WithVerifierOpts( + rp.WithIssuedAtOffset(5*time.Second), + rp.WithSupportedSigningAlgorithms("RS256", "RS384", "RS512", "ES256", "ES384", "ES512"), + ), + ) + require.NoError(t, err, "new rp") + + t.Log("------- start auth flow with prompt=none ------- ") + state := "state-32892" + capturedW := httptest.NewRecorder() + localURL, err := url.Parse(targetURL + "/login") + require.NoError(t, err) + + get := httptest.NewRequest("GET", localURL.String(), nil) + rp.AuthURLHandler(func() string { return state }, provider, + rp.WithPromptURLParam("none"), + rp.WithResponseModeURLParam(oidc.ResponseModeFragment), + )(capturedW, get) + + defer func() { + if t.Failed() { + t.Log("response body (redirect from RP to OP)", capturedW.Body.String()) + } + }() + require.GreaterOrEqual(t, capturedW.Code, 200, "captured response code") + require.Less(t, capturedW.Code, 400, "captured response code") + + //nolint:bodyclose + resp := capturedW.Result() + jar.SetCookies(localURL, resp.Cookies()) + + startAuthURL, err := resp.Location() + require.NoError(t, err, "get redirect") + assert.NotEmpty(t, startAuthURL, "login url") + t.Log("Starting auth at", startAuthURL) + + t.Log("------- get redirect from OP ------") + loginPageURL := getRedirect(t, "get redirect to login page", httpClient, startAuthURL) + t.Log("login page URL", loginPageURL) + + require.Contains(t, loginPageURL.String(), `error=login_required`, "prompt=none should error") + require.Contains(t, loginPageURL.String(), `local-site#error=`, "response_mode=fragment means '#' instead of '?'") +} + type deferredHandler struct { http.Handler } diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 108fa4f..114599d 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -569,6 +569,11 @@ func WithPromptURLParam(prompt ...string) URLParamOpt { return withPrompt(prompt...) } +// WithResponseModeURLParam sets the `response_mode` parameter in a URL. +func WithResponseModeURLParam(mode oidc.ResponseMode) URLParamOpt { + return withURLParam("response_mode", string(mode)) +} + type AuthURLOpt func() []oauth2.AuthCodeOption // WithCodeChallenge sets the `code_challenge` params in the auth request diff --git a/pkg/oidc/authorization.go b/pkg/oidc/authorization.go index f620ecb..ace1de1 100644 --- a/pkg/oidc/authorization.go +++ b/pkg/oidc/authorization.go @@ -60,7 +60,7 @@ const ( ) // AuthRequest according to: -//https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest +// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest type AuthRequest struct { Scopes SpaceDelimitedArray `json:"scope" schema:"scope"` ResponseType ResponseType `json:"response_type" schema:"response_type"` @@ -100,3 +100,8 @@ func (a *AuthRequest) GetResponseType() ResponseType { func (a *AuthRequest) GetState() string { return a.State } + +// GetResponseMode returns the optional ResponseMode +func (a *AuthRequest) GetResponseMode() ResponseMode { + return a.ResponseMode +} From d5a9bd6d0e798d43c0d8cc4ea62347bcce18548a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 5 May 2023 14:36:37 +0200 Subject: [PATCH 233/502] feat: generic Userinfo and Introspect functions (#389) BREAKING CHANGE: rp.Userinfo and rs.Introspect now require a type parameter. --- example/client/api/api.go | 4 +- pkg/client/rp/relying_party.go | 30 +++++++++----- pkg/client/rp/userinfo_example_test.go | 45 ++++++++++++++++++++ pkg/client/rs/introspect_example_test.go | 52 ++++++++++++++++++++++++ pkg/client/rs/resource_server.go | 18 +++++--- pkg/oidc/userinfo.go | 5 +++ 6 files changed, 136 insertions(+), 18 deletions(-) create mode 100644 pkg/client/rp/userinfo_example_test.go create mode 100644 pkg/client/rs/introspect_example_test.go diff --git a/example/client/api/api.go b/example/client/api/api.go index 83ec2a1..2f81c07 100644 --- a/example/client/api/api.go +++ b/example/client/api/api.go @@ -48,7 +48,7 @@ func main() { if !ok { return } - resp, err := rs.Introspect(r.Context(), provider, token) + resp, err := rs.Introspect[*oidc.IntrospectionResponse](r.Context(), provider, token) if err != nil { http.Error(w, err.Error(), http.StatusForbidden) return @@ -69,7 +69,7 @@ func main() { if !ok { return } - resp, err := rs.Introspect(r.Context(), provider, token) + resp, err := rs.Introspect[*oidc.IntrospectionResponse](r.Context(), provider, token) if err != nil { http.Error(w, err.Error(), http.StatusForbidden) return diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index b93a373..7d73a5a 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -435,14 +435,18 @@ func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp R } } -type CodeExchangeUserinfoCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, provider RelyingParty, info *oidc.UserInfo) +type SubjectGetter interface { + GetSubject() string +} + +type CodeExchangeUserinfoCallback[C oidc.IDClaims, U SubjectGetter] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, provider RelyingParty, info U) // UserinfoCallback wraps the callback function of the CodeExchangeHandler // and calls the userinfo endpoint with the access token // on success it will pass the userinfo into its callback function as well -func UserinfoCallback[C oidc.IDClaims](f CodeExchangeUserinfoCallback[C]) CodeExchangeCallback[C] { +func UserinfoCallback[C oidc.IDClaims, U SubjectGetter](f CodeExchangeUserinfoCallback[C, U]) CodeExchangeCallback[C] { return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) { - info, err := Userinfo(r.Context(), tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp) + info, err := Userinfo[U](r.Context(), tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp) if err != nil { http.Error(w, "userinfo failed: "+err.Error(), http.StatusUnauthorized) return @@ -451,19 +455,25 @@ func UserinfoCallback[C oidc.IDClaims](f CodeExchangeUserinfoCallback[C]) CodeEx } } -// Userinfo will call the OIDC Userinfo Endpoint with the provided token -func Userinfo(ctx context.Context, token, tokenType, subject string, rp RelyingParty) (*oidc.UserInfo, error) { +// Userinfo will call the OIDC [UserInfo] Endpoint with the provided token and returns +// the response in an instance of type U. +// [*oidc.UserInfo] can be used as a good example, or use a custom type if type-safe +// access to custom claims is needed. +// +// [UserInfo]: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo +func Userinfo[U SubjectGetter](ctx context.Context, token, tokenType, subject string, rp RelyingParty) (userinfo U, err error) { + var nilU U + req, err := http.NewRequestWithContext(ctx, http.MethodGet, rp.UserinfoEndpoint(), nil) if err != nil { - return nil, err + return nilU, err } req.Header.Set("authorization", tokenType+" "+token) - userinfo := new(oidc.UserInfo) if err := httphelper.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil { - return nil, err + return nilU, err } - if userinfo.Subject != subject { - return nil, ErrUserInfoSubNotMatching + if userinfo.GetSubject() != subject { + return nilU, ErrUserInfoSubNotMatching } return userinfo, nil } diff --git a/pkg/client/rp/userinfo_example_test.go b/pkg/client/rp/userinfo_example_test.go new file mode 100644 index 0000000..2cc5222 --- /dev/null +++ b/pkg/client/rp/userinfo_example_test.go @@ -0,0 +1,45 @@ +package rp_test + +import ( + "context" + "fmt" + + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" +) + +type UserInfo struct { + Subject string `json:"sub,omitempty"` + oidc.UserInfoProfile + oidc.UserInfoEmail + oidc.UserInfoPhone + Address *oidc.UserInfoAddress `json:"address,omitempty"` + + // Foo and Bar are custom claims + Foo string `json:"foo,omitempty"` + Bar struct { + Val1 string `json:"val_1,omitempty"` + Val2 string `json:"val_2,omitempty"` + } `json:"bar,omitempty"` + + // Claims are all the combined claims, including custom. + Claims map[string]any `json:"-,omitempty"` +} + +func (u *UserInfo) GetSubject() string { + return u.Subject +} + +func ExampleUserinfo_custom() { + rpo, err := rp.NewRelyingPartyOIDC(context.TODO(), "http://localhost:8080", "clientid", "clientsecret", "http://example.com/redirect", []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopePhone}) + if err != nil { + panic(err) + } + + info, err := rp.Userinfo[*UserInfo](context.TODO(), "accesstokenstring", "Bearer", "userid", rpo) + if err != nil { + panic(err) + } + + fmt.Println(info) +} diff --git a/pkg/client/rs/introspect_example_test.go b/pkg/client/rs/introspect_example_test.go new file mode 100644 index 0000000..eac8be2 --- /dev/null +++ b/pkg/client/rs/introspect_example_test.go @@ -0,0 +1,52 @@ +package rs_test + +import ( + "context" + "fmt" + + "github.com/zitadel/oidc/v3/pkg/client/rs" + "github.com/zitadel/oidc/v3/pkg/oidc" +) + +type IntrospectionResponse struct { + Active bool `json:"active"` + Scope oidc.SpaceDelimitedArray `json:"scope,omitempty"` + ClientID string `json:"client_id,omitempty"` + TokenType string `json:"token_type,omitempty"` + Expiration oidc.Time `json:"exp,omitempty"` + IssuedAt oidc.Time `json:"iat,omitempty"` + NotBefore oidc.Time `json:"nbf,omitempty"` + Subject string `json:"sub,omitempty"` + Audience oidc.Audience `json:"aud,omitempty"` + Issuer string `json:"iss,omitempty"` + JWTID string `json:"jti,omitempty"` + Username string `json:"username,omitempty"` + oidc.UserInfoProfile + oidc.UserInfoEmail + oidc.UserInfoPhone + Address *oidc.UserInfoAddress `json:"address,omitempty"` + + // Foo and Bar are custom claims + Foo string `json:"foo,omitempty"` + Bar struct { + Val1 string `json:"val_1,omitempty"` + Val2 string `json:"val_2,omitempty"` + } `json:"bar,omitempty"` + + // Claims are all the combined claims, including custom. + Claims map[string]any `json:"-,omitempty"` +} + +func ExampleIntrospect_custom() { + rss, err := rs.NewResourceServerClientCredentials(context.TODO(), "http://localhost:8080", "clientid", "clientsecret") + if err != nil { + panic(err) + } + + resp, err := rs.Introspect[*IntrospectionResponse](context.TODO(), rss, "accesstokenstring") + if err != nil { + panic(err) + } + + fmt.Println(resp) +} diff --git a/pkg/client/rs/resource_server.go b/pkg/client/rs/resource_server.go index 054dfbe..49232b2 100644 --- a/pkg/client/rs/resource_server.go +++ b/pkg/client/rs/resource_server.go @@ -112,18 +112,24 @@ func WithStaticEndpoints(tokenURL, introspectURL string) Option { } } -func Introspect(ctx context.Context, rp ResourceServer, token string) (*oidc.IntrospectionResponse, error) { +// Introspect calls the [RFC7662] Token Introspection +// endpoint and returns the response in an instance of type R. +// [*oidc.IntrospectionResponse] can be used as a good example, or use a custom type if type-safe +// access to custom claims is needed. +// +// [RFC7662]: https://www.rfc-editor.org/rfc/rfc7662 +func Introspect[R any](ctx context.Context, rp ResourceServer, token string) (resp R, err error) { authFn, err := rp.AuthFn() if err != nil { - return nil, err + return resp, err } req, err := httphelper.FormRequest(ctx, rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, client.Encoder, authFn) if err != nil { - return nil, err + return resp, err } - resp := new(oidc.IntrospectionResponse) - if err := httphelper.HttpRequest(rp.HttpClient(), req, resp); err != nil { - return nil, err + + if err := httphelper.HttpRequest(rp.HttpClient(), req, &resp); err != nil { + return resp, err } return resp, nil } diff --git a/pkg/oidc/userinfo.go b/pkg/oidc/userinfo.go index caff58e..ef8ebe4 100644 --- a/pkg/oidc/userinfo.go +++ b/pkg/oidc/userinfo.go @@ -29,6 +29,11 @@ func (u *UserInfo) GetAddress() *UserInfoAddress { return u.Address } +// GetSubject implements [rp.SubjectGetter] +func (u *UserInfo) GetSubject() string { + return u.Subject +} + type uiAlias UserInfo func (u *UserInfo) MarshalJSON() ([]byte, error) { From 50271a9c19820c67d646ad76ad3bc003d8291768 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 May 2023 18:43:59 +0200 Subject: [PATCH 234/502] chore(deps): bump golang.org/x/oauth2 from 0.7.0 to 0.8.0 (#391) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.7.0 to 0.8.0. - [Commits](https://github.com/golang/oauth2/compare/v0.7.0...v0.8.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 5f96c72..86c4f20 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/rs/cors v1.9.0 github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.2 - golang.org/x/oauth2 v0.7.0 + golang.org/x/oauth2 v0.8.0 golang.org/x/text v0.9.0 gopkg.in/square/go-jose.v2 v2.6.0 ) @@ -26,8 +26,8 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/crypto v0.7.0 // indirect - golang.org/x/net v0.9.0 // indirect - golang.org/x/sys v0.7.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.29.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index df4d60c..0e3906a 100644 --- a/go.sum +++ b/go.sum @@ -60,11 +60,11 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= -golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= +golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -73,8 +73,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= From 0b916d9b6986821e884ac42eaa73713845b626ee Mon Sep 17 00:00:00 2001 From: Fabi Date: Fri, 12 May 2023 06:57:41 +0200 Subject: [PATCH 235/502] docs: pull request template (#386) * docs: pull request template * Rename pull_request_template to pull_request_template.md --- .github/pull_request_template.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..811b195 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +### Definition of Ready + +- [ ] I am happy with the code +- [ ] Short description of the feature/issue is added in the pr description +- [ ] PR is linked to the corresponding user story +- [ ] Acceptance criteria are met +- [ ] All open todos and follow ups are defined in a new ticket and justified +- [ ] Deviations from the acceptance criteria and design are agreed with the PO and documented. +- [ ] No debug or dead code +- [ ] Critical parts are tested automatically +- [ ] Where possible E2E tests are implemented +- [ ] Documentation/examples are up-to-date +- [ ] All non-functional requirements are met +- [ ] Functionality of the acceptance criteria is checked manually on the dev system. + From 8d0819ee8ac9fa537b6b3b417118cc8dd99608d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 May 2023 08:12:21 +0200 Subject: [PATCH 236/502] chore(deps): bump github.com/sirupsen/logrus from 1.9.0 to 1.9.1 (#392) Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.9.0 to 1.9.1. - [Release notes](https://github.com/sirupsen/logrus/releases) - [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md) - [Commits](https://github.com/sirupsen/logrus/compare/v1.9.0...v1.9.1) --- updated-dependencies: - dependency-name: github.com/sirupsen/logrus dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 86c4f20..b6393e4 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/jeremija/gosubmit v0.2.7 github.com/muhlemmer/gu v0.3.1 github.com/rs/cors v1.9.0 - github.com/sirupsen/logrus v1.9.0 + github.com/sirupsen/logrus v1.9.1 github.com/stretchr/testify v1.8.2 golang.org/x/oauth2 v0.8.0 golang.org/x/text v0.9.0 diff --git a/go.sum b/go.sum index 0e3906a..959fb6b 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.1 h1:Ou41VVR3nMWWmTiEUnj0OlsgOSCUFgsPAOl6jRIcVtQ= +github.com/sirupsen/logrus v1.9.1/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= From e9c1bec01efc62b252ee789cee5453f672ae4d64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 May 2023 12:31:23 +0300 Subject: [PATCH 237/502] chore(deps): bump github.com/stretchr/testify from 1.8.2 to 1.8.3 (#395) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.2 to 1.8.3. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.8.2...v1.8.3) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index b6393e4..ddd5028 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/muhlemmer/gu v0.3.1 github.com/rs/cors v1.9.0 github.com/sirupsen/logrus v1.9.1 - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.8.3 golang.org/x/oauth2 v0.8.0 golang.org/x/text v0.9.0 gopkg.in/square/go-jose.v2 v2.6.0 diff --git a/go.sum b/go.sum index 959fb6b..e16d648 100644 --- a/go.sum +++ b/go.sum @@ -41,13 +41,9 @@ github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/sirupsen/logrus v1.9.1 h1:Ou41VVR3nMWWmTiEUnj0OlsgOSCUFgsPAOl6jRIcVtQ= github.com/sirupsen/logrus v1.9.1/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= From d1dfb284e5ecac2999e0999e1cc89b88c3111d0d Mon Sep 17 00:00:00 2001 From: Fabi Date: Mon, 22 May 2023 09:21:52 +0200 Subject: [PATCH 238/502] docs: add dry to pr template --- .github/pull_request_template.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 811b195..6c4ae58 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -7,6 +7,7 @@ - [ ] All open todos and follow ups are defined in a new ticket and justified - [ ] Deviations from the acceptance criteria and design are agreed with the PO and documented. - [ ] No debug or dead code +- [ ] My code has no repetitions - [ ] Critical parts are tested automatically - [ ] Where possible E2E tests are implemented - [ ] Documentation/examples are up-to-date From 268e72420f0716a887128a56c0dbac72594101a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 May 2023 13:37:23 +0300 Subject: [PATCH 239/502] chore(deps): bump codecov/codecov-action from 3.1.3 to 3.1.4 (#397) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3.1.3 to 3.1.4. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v3.1.3...v3.1.4) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ffd9c8..b21e7e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v3.1.3 + - uses: codecov/codecov-action@v3.1.4 with: file: ./profile.cov name: codecov-go From 941ed10780a6e5569b3dba3fe19d26400601773d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 10:38:47 +0300 Subject: [PATCH 240/502] chore(deps): bump github.com/sirupsen/logrus from 1.9.1 to 1.9.2 (#394) Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.9.1 to 1.9.2. - [Release notes](https://github.com/sirupsen/logrus/releases) - [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md) - [Commits](https://github.com/sirupsen/logrus/compare/v1.9.1...v1.9.2) --- updated-dependencies: - dependency-name: github.com/sirupsen/logrus dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ddd5028..91028bc 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/jeremija/gosubmit v0.2.7 github.com/muhlemmer/gu v0.3.1 github.com/rs/cors v1.9.0 - github.com/sirupsen/logrus v1.9.1 + github.com/sirupsen/logrus v1.9.2 github.com/stretchr/testify v1.8.3 golang.org/x/oauth2 v0.8.0 golang.org/x/text v0.9.0 diff --git a/go.sum b/go.sum index e16d648..6919056 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/sirupsen/logrus v1.9.1 h1:Ou41VVR3nMWWmTiEUnj0OlsgOSCUFgsPAOl6jRIcVtQ= -github.com/sirupsen/logrus v1.9.1/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= +github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= From 09bdd1dca291f319a9e9df0fbff6be0f5112a04e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 24 May 2023 10:39:11 +0300 Subject: [PATCH 241/502] fix: token type from client for device auth (#398) --- pkg/op/device.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/op/device.go b/pkg/op/device.go index f7691ca..397cede 100644 --- a/pkg/op/device.go +++ b/pkg/op/device.go @@ -271,8 +271,8 @@ func CheckDeviceAuthorizationState(ctx context.Context, clientID, deviceCode str return state, oidc.ErrAuthorizationPending() } -func CreateDeviceTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator, client AccessTokenClient) (*oidc.AccessTokenResponse, error) { - accessToken, refreshToken, validity, err := CreateAccessToken(ctx, tokenRequest, AccessTokenTypeBearer, creator, client, "") +func CreateDeviceTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator, client Client) (*oidc.AccessTokenResponse, error) { + accessToken, refreshToken, validity, err := CreateAccessToken(ctx, tokenRequest, client.AccessTokenType(), creator, client, "") if err != nil { return nil, err } From e8262cbf1fc224e8a7447cfc3d1ffa93c1a70b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 26 May 2023 11:06:33 +0300 Subject: [PATCH 242/502] chore: cleanup unneeded device storage methods (#399) BREAKING CHANGE, removes methods from DeviceAuthorizationStorage: - GetDeviceAuthorizationByUserCode - CompleteDeviceAuthorization - DenyDeviceAuthorization The methods are now moved to examples as something similar can be userful for implementers. --- example/server/exampleop/device.go | 13 +++++++++++++ pkg/op/device_test.go | 5 +++-- pkg/op/storage.go | 12 ------------ 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/example/server/exampleop/device.go b/example/server/exampleop/device.go index 0dda3d5..7478750 100644 --- a/example/server/exampleop/device.go +++ b/example/server/exampleop/device.go @@ -1,6 +1,7 @@ package exampleop import ( + "context" "errors" "fmt" "io" @@ -16,6 +17,18 @@ import ( type deviceAuthenticate interface { CheckUsernamePasswordSimple(username, password string) error op.DeviceAuthorizationStorage + + // GetDeviceAuthorizationByUserCode resturns the current state of the device authorization flow, + // identified by the user code. + GetDeviceAuthorizationByUserCode(ctx context.Context, userCode string) (*op.DeviceAuthorizationState, error) + + // CompleteDeviceAuthorization marks a device authorization entry as Completed, + // identified by userCode. The Subject is added to the state, so that + // GetDeviceAuthorizatonState can use it to create a new Access Token. + CompleteDeviceAuthorization(ctx context.Context, userCode, subject string) error + + // DenyDeviceAuthorization marks a device authorization entry as Denied. + DenyDeviceAuthorization(ctx context.Context, userCode string) error } type deviceLogin struct { diff --git a/pkg/op/device_test.go b/pkg/op/device_test.go index 1e32554..5fe6e27 100644 --- a/pkg/op/device_test.go +++ b/pkg/op/device_test.go @@ -16,6 +16,7 @@ import ( "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/zitadel/oidc/v3/example/server/storage" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" ) @@ -304,7 +305,7 @@ func BenchmarkNewUserCode(b *testing.B) { } func TestDeviceAccessToken(t *testing.T) { - storage := testProvider.Storage().(op.DeviceAuthorizationStorage) + storage := testProvider.Storage().(*storage.Storage) storage.StoreDeviceAuthorization(context.Background(), "native", "qwerty", "yuiop", time.Now().Add(time.Minute), []string{"foo"}) storage.CompleteDeviceAuthorization(context.Background(), "yuiop", "tim") @@ -329,7 +330,7 @@ func TestDeviceAccessToken(t *testing.T) { func TestCheckDeviceAuthorizationState(t *testing.T) { now := time.Now() - storage := testProvider.Storage().(op.DeviceAuthorizationStorage) + storage := testProvider.Storage().(*storage.Storage) storage.StoreDeviceAuthorization(context.Background(), "native", "pending", "pending", now.Add(time.Minute), []string{"foo"}) storage.StoreDeviceAuthorization(context.Background(), "native", "denied", "denied", now.Add(time.Minute), []string{"foo"}) storage.StoreDeviceAuthorization(context.Background(), "native", "completed", "completed", now.Add(time.Minute), []string{"foo"}) diff --git a/pkg/op/storage.go b/pkg/op/storage.go index aa8721a..23d2133 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -182,18 +182,6 @@ type DeviceAuthorizationStorage interface { // GetDeviceAuthorizatonState returns the current state of the device authorization flow in the database. // The method is polled untill the the authorization is eighter Completed, Expired or Denied. GetDeviceAuthorizatonState(ctx context.Context, clientID, deviceCode string) (*DeviceAuthorizationState, error) - - // GetDeviceAuthorizationByUserCode resturn the current state of the device authorization flow, - // identified by the user code. - GetDeviceAuthorizationByUserCode(ctx context.Context, userCode string) (*DeviceAuthorizationState, error) - - // CompleteDeviceAuthorization marks a device authorization entry as Completed, - // identified by userCode. The Subject is added to the state, so that - // GetDeviceAuthorizatonState can use it to create a new Access Token. - CompleteDeviceAuthorization(ctx context.Context, userCode, subject string) error - - // DenyDeviceAuthorization marks a device authorization entry as Denied. - DenyDeviceAuthorization(ctx context.Context, userCode string) error } func assertDeviceStorage(s Storage) (DeviceAuthorizationStorage, error) { From a4dbe2a973ad0e6dbb264d52477240964dd7acbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 26 May 2023 11:52:35 +0300 Subject: [PATCH 243/502] fix: enforce device authorization grant type (#400) --- example/server/storage/client.go | 18 ++++++++++++++++++ pkg/op/device.go | 7 +++++++ pkg/op/device_test.go | 23 +++++++++++++++++++---- pkg/op/op_test.go | 3 ++- 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/example/server/storage/client.go b/example/server/storage/client.go index b8b9960..b28d9d4 100644 --- a/example/server/storage/client.go +++ b/example/server/storage/client.go @@ -193,6 +193,24 @@ func WebClient(id, secret string, redirectURIs ...string) *Client { } } +// DeviceClient creates a device client with Basic authentication. +func DeviceClient(id, secret string) *Client { + return &Client{ + id: id, + secret: secret, + redirectURIs: nil, + applicationType: op.ApplicationTypeWeb, + authMethod: oidc.AuthMethodBasic, + loginURL: defaultLoginURL, + responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode}, + grantTypes: []oidc.GrantType{oidc.GrantTypeDeviceCode}, + accessTokenType: op.AccessTokenTypeBearer, + devMode: false, + idTokenUserinfoClaimsAssertion: false, + clockSkew: 0, + } +} + type hasRedirectGlobs struct { *Client } diff --git a/pkg/op/device.go b/pkg/op/device.go index 397cede..f584c31 100644 --- a/pkg/op/device.go +++ b/pkg/op/device.go @@ -122,6 +122,13 @@ func ParseDeviceCodeRequest(r *http.Request, o OpenIDProvider) (*oidc.DeviceAuth if err != nil { return nil, err } + client, err := o.Storage().GetClientByClientID(r.Context(), clientID) + if err != nil { + return nil, err + } + if !ValidateGrantType(client, oidc.GrantTypeDeviceCode) { + return nil, oidc.ErrUnauthorizedClient().WithDescription("client missing grant type " + string(oidc.GrantTypeCode)) + } req := new(oidc.DeviceAuthorizationRequest) if err := o.Decoder().Decode(req, r.Form); err != nil { diff --git a/pkg/op/device_test.go b/pkg/op/device_test.go index cf94c3f..4b3c98c 100644 --- a/pkg/op/device_test.go +++ b/pkg/op/device_test.go @@ -51,7 +51,7 @@ func Test_deviceAuthorizationHandler(t *testing.T) { req := &oidc.DeviceAuthorizationRequest{ Scopes: []string{"foo", "bar"}, - ClientID: "web", + ClientID: "device", } values := make(url.Values) testProvider.Encoder().Encode(req, values) @@ -88,11 +88,27 @@ func TestParseDeviceCodeRequest(t *testing.T) { wantErr: true, }, { - name: "success", + name: "missing grant type", req: &oidc.DeviceAuthorizationRequest{ Scopes: oidc.SpaceDelimitedArray{"foo", "bar"}, ClientID: "web", }, + wantErr: true, + }, + { + name: "client not found", + req: &oidc.DeviceAuthorizationRequest{ + Scopes: oidc.SpaceDelimitedArray{"foo", "bar"}, + ClientID: "foobar", + }, + wantErr: true, + }, + { + name: "success", + req: &oidc.DeviceAuthorizationRequest{ + Scopes: oidc.SpaceDelimitedArray{"foo", "bar"}, + ClientID: "device", + }, }, } for _, tt := range tests { @@ -110,8 +126,7 @@ func TestParseDeviceCodeRequest(t *testing.T) { got, err := op.ParseDeviceCodeRequest(r, testProvider) if tt.wantErr { require.Error(t, err) - } else { - require.NoError(t, err) + return } assert.Equal(t, tt.req, got) }) diff --git a/pkg/op/op_test.go b/pkg/op/op_test.go index 3e6377f..b637e03 100644 --- a/pkg/op/op_test.go +++ b/pkg/op/op_test.go @@ -49,6 +49,7 @@ func init() { storage.RegisterClients( storage.NativeClient("native"), storage.WebClient("web", "secret", "https://example.com"), + storage.DeviceClient("device", "secret"), storage.WebClient("api", "secret"), ) @@ -336,7 +337,7 @@ func TestRoutes(t *testing.T) { name: "device authorization", method: http.MethodGet, path: testProvider.DeviceAuthorizationEndpoint().Relative(), - basicAuth: &basicAuth{"web", "secret"}, + basicAuth: &basicAuth{"device", "secret"}, values: map[string]string{ "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(), }, From d693ed0e8c4ad2d072082898fc279ecfec49ff75 Mon Sep 17 00:00:00 2001 From: Fabi Date: Wed, 31 May 2023 14:07:04 +0200 Subject: [PATCH 244/502] docs: issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 38 ----------------- .github/ISSUE_TEMPLATE/bug_report.yaml | 57 ++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 38 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 49ccc49..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: 🐛 Bug report -about: Create a report to help us improve -title: '' -labels: bug -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** -- OS: [e.g. iOS] -- Browser [e.g. chrome, safari] -- Version [e.g. 22] - -**Smartphone (please complete the following information):** -- Device: [e.g. iPhone6] -- OS: [e.g. iOS8.1] -- Browser [e.g. stock browser, safari] -- Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000..929595e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,57 @@ +name: 🐛 Bug Report +description: "Create a bug report to help us improve ZITADEL Terrafform Provider." +title: "" +labels: ["bug"] +body: +- type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! +- type: checkboxes + id: preflight + attributes: + label: Preflight Checklist + options: + - label: + I could not find a solution in the documentation, the existing issues or discussions + required: true + - label: + I have joined the [ZITADEL chat](https://zitadel.com/chat) +- type: input + id: version + attributes: + label: Version + required: true + description: Which version of the OIDC Library are you using. +- type: textarea + id: impact + attributes: + label: Describe the problems caused by this bug + description: A clear and concise description of what the problems you face and the bug you have. + validations: + required: true +- type: textarea + id: reproduce + attributes: + label: To reproduce + description: Steps to reproduce the behaviour + placeholder: | + Steps to reproduce the behavior: + validations: + required: true +- type: textarea + id: screenshots + attributes: + label: Screenshots + description: If applicable, add screenshots to help explain your problem. +- type: textarea + id: expected + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + placeholder: As a [type of user], I want [some goal] so that [some reason]. +- type: textarea + id: additional + attributes: + label: Additional Context + description: Please add any other infos that could be useful. From 9d60a4b1831071d75dfdef209d70de9b60e6bf71 Mon Sep 17 00:00:00 2001 From: Fabi Date: Wed, 31 May 2023 14:08:05 +0200 Subject: [PATCH 245/502] docs: add issue template for docs --- .github/ISSUE_TEMPLATE/docs.yaml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/docs.yaml diff --git a/.github/ISSUE_TEMPLATE/docs.yaml b/.github/ISSUE_TEMPLATE/docs.yaml new file mode 100644 index 0000000..cbe7358 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/docs.yaml @@ -0,0 +1,31 @@ +name: 📄 Documentation +description: Create an issue for missing or wrong documentation. +title: "" +labels: ["docs"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this issue. + - type: checkboxes + id: preflight + attributes: + label: Preflight Checklist + options: + - label: + I could not find a solution in the existing issues, docs, nor discussions + required: true + - label: + I have joined the [ZITADEL chat](https://zitadel.com/chat) + - type: textarea + id: docs + attributes: + label: Describe the docs your are missing or that are wrong + placeholder: As a [type of user], I want [some goal] so that [some reason]. + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional Context + description: Please add any other infos that could be useful. From 087a0eb0a94b0e8f2aea24556b9e6d004fef2238 Mon Sep 17 00:00:00 2001 From: Fabi Date: Wed, 31 May 2023 14:09:07 +0200 Subject: [PATCH 246/502] docs: proposal issue template --- .github/ISSUE_TEMPLATE/feature_request.md | 20 ---------- .github/ISSUE_TEMPLATE/proposal.yaml | 45 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 20 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/proposal.yaml diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 118d30e..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: 🚀 Feature request -about: Suggest an idea for this project -title: '' -labels: enhancement -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/proposal.yaml b/.github/ISSUE_TEMPLATE/proposal.yaml new file mode 100644 index 0000000..f1a189c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/proposal.yaml @@ -0,0 +1,45 @@ +name: 💡 Proposal / Feature request +description: +title: "" +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this proposal / feature reqeust + - type: checkboxes + id: preflight + attributes: + label: Preflight Checklist + options: + - label: + I could not find a solution in the existing issues, docs, nor discussions + required: true + - label: + I have joined the [ZITADEL chat](https://zitadel.com/chat) + - type: textarea + id: problem + attributes: + label: Describe your problem + description: Please describe your problem this proposal / feature is supposed to solve. + placeholder: Describe the problem you have. + validations: + required: true + - type: textarea + id: solution + attributes: + label: Describe your ideal solution + description: Which solution do you propose? + placeholder: As a [type of user], I want [some goal] so that [some reason]. + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Which version of the OIDC Library are you using. + - type: textarea + id: additional + attributes: + label: Additional Context + description: Please add any other infos that could be useful. From af14335eb09bbd2973c40f928268db217394a959 Mon Sep 17 00:00:00 2001 From: Fabi Date: Wed, 31 May 2023 14:09:27 +0200 Subject: [PATCH 247/502] docs: remove title --- .github/ISSUE_TEMPLATE/proposal.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/proposal.yaml b/.github/ISSUE_TEMPLATE/proposal.yaml index f1a189c..8c3518b 100644 --- a/.github/ISSUE_TEMPLATE/proposal.yaml +++ b/.github/ISSUE_TEMPLATE/proposal.yaml @@ -1,6 +1,6 @@ name: 💡 Proposal / Feature request description: -title: "" +title: labels: ["enhancement"] body: - type: markdown From 96da29a6d1c5e9a4a589d1596acc5d3cb78eff4c Mon Sep 17 00:00:00 2001 From: Fabi Date: Wed, 31 May 2023 14:09:47 +0200 Subject: [PATCH 248/502] docs: fix title --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 929595e..900422c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,6 +1,6 @@ name: 🐛 Bug Report description: "Create a bug report to help us improve ZITADEL Terrafform Provider." -title: "" +title: labels: ["bug"] body: - type: markdown From 54a071f27b61c46b6658d960c80ebb147c807b64 Mon Sep 17 00:00:00 2001 From: Fabi Date: Wed, 31 May 2023 14:11:14 +0200 Subject: [PATCH 249/502] Update bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 104 ++++++++++++------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 900422c..830f6c1 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,57 +1,57 @@ name: 🐛 Bug Report description: "Create a bug report to help us improve ZITADEL Terrafform Provider." -title: +title: labels: ["bug"] body: -- type: markdown - attributes: - value: | - Thanks for taking the time to fill out this bug report! -- type: checkboxes - id: preflight - attributes: - label: Preflight Checklist - options: - - label: - I could not find a solution in the documentation, the existing issues or discussions + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: checkboxes + id: preflight + attributes: + label: Preflight Checklist + options: + - label: + I could not find a solution in the documentation, the existing issues or discussions + required: true + - label: + I have joined the [ZITADEL chat](https://zitadel.com/chat) + - type: input + id: version + attributes: + label: Version required: true - - label: - I have joined the [ZITADEL chat](https://zitadel.com/chat) -- type: input - id: version - attributes: - label: Version - required: true - description: Which version of the OIDC Library are you using. -- type: textarea - id: impact - attributes: - label: Describe the problems caused by this bug - description: A clear and concise description of what the problems you face and the bug you have. - validations: - required: true -- type: textarea - id: reproduce - attributes: - label: To reproduce - description: Steps to reproduce the behaviour - placeholder: | - Steps to reproduce the behavior: - validations: - required: true -- type: textarea - id: screenshots - attributes: - label: Screenshots - description: If applicable, add screenshots to help explain your problem. -- type: textarea - id: expected - attributes: - label: Expected behavior - description: A clear and concise description of what you expected to happen. - placeholder: As a [type of user], I want [some goal] so that [some reason]. -- type: textarea - id: additional - attributes: - label: Additional Context - description: Please add any other infos that could be useful. + description: Which version of the OIDC Library are you using. + - type: textarea + id: impact + attributes: + label: Describe the problems caused by this bug + description: A clear and concise description of what the problems you face and the bug you have. + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: To reproduce + description: Steps to reproduce the behaviour + placeholder: | + Steps to reproduce the behavior: + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: If applicable, add screenshots to help explain your problem. + - type: textarea + id: expected + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + placeholder: As a [type of user], I want [some goal] so that [some reason]. + - type: textarea + id: additional + attributes: + label: Additional Context + description: Please add any other infos that could be useful. From c3bed1d2ec0af0c8c7acbadaf5c0e5d4f87816fe Mon Sep 17 00:00:00 2001 From: Fabi Date: Wed, 31 May 2023 14:11:32 +0200 Subject: [PATCH 250/502] Update bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 830f6c1..e24626a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,6 +1,6 @@ name: 🐛 Bug Report description: "Create a bug report to help us improve ZITADEL Terrafform Provider." -title: +title: labels: ["bug"] body: - type: markdown From 6607c5a6907aad740889b675f418efece73a724e Mon Sep 17 00:00:00 2001 From: Fabi Date: Wed, 31 May 2023 14:12:13 +0200 Subject: [PATCH 251/502] Update docs.yaml --- .github/ISSUE_TEMPLATE/docs.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/docs.yaml b/.github/ISSUE_TEMPLATE/docs.yaml index cbe7358..04c1c0c 100644 --- a/.github/ISSUE_TEMPLATE/docs.yaml +++ b/.github/ISSUE_TEMPLATE/docs.yaml @@ -1,6 +1,5 @@ name: 📄 Documentation description: Create an issue for missing or wrong documentation. -title: "" labels: ["docs"] body: - type: markdown From 96ff038c673ad26d22c9f671d9a9f659a827d524 Mon Sep 17 00:00:00 2001 From: Fabi Date: Wed, 31 May 2023 14:12:46 +0200 Subject: [PATCH 252/502] Update proposal.yaml --- .github/ISSUE_TEMPLATE/proposal.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/proposal.yaml b/.github/ISSUE_TEMPLATE/proposal.yaml index 8c3518b..d308a57 100644 --- a/.github/ISSUE_TEMPLATE/proposal.yaml +++ b/.github/ISSUE_TEMPLATE/proposal.yaml @@ -1,6 +1,4 @@ name: 💡 Proposal / Feature request -description: -title: labels: ["enhancement"] body: - type: markdown From ae2d2f625657dc5482124f56c9657f6229c8d799 Mon Sep 17 00:00:00 2001 From: Fabi Date: Wed, 31 May 2023 14:13:14 +0200 Subject: [PATCH 253/502] Update proposal.yaml --- .github/ISSUE_TEMPLATE/proposal.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/proposal.yaml b/.github/ISSUE_TEMPLATE/proposal.yaml index d308a57..a3db2b1 100644 --- a/.github/ISSUE_TEMPLATE/proposal.yaml +++ b/.github/ISSUE_TEMPLATE/proposal.yaml @@ -1,4 +1,5 @@ name: 💡 Proposal / Feature request +description: labels: ["enhancement"] body: - type: markdown From 3f3429eedeadd261c4d7ebb553eb36422bade2a7 Mon Sep 17 00:00:00 2001 From: Fabi Date: Wed, 31 May 2023 16:37:53 +0200 Subject: [PATCH 254/502] Update proposal.yaml --- .github/ISSUE_TEMPLATE/proposal.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/proposal.yaml b/.github/ISSUE_TEMPLATE/proposal.yaml index a3db2b1..af7acd5 100644 --- a/.github/ISSUE_TEMPLATE/proposal.yaml +++ b/.github/ISSUE_TEMPLATE/proposal.yaml @@ -1,5 +1,5 @@ name: 💡 Proposal / Feature request -description: +description: "Create an issue for a feature request/proposal." labels: ["enhancement"] body: - type: markdown From f838acb7c37a3814271da9fd62d674ed1bdd71cc Mon Sep 17 00:00:00 2001 From: Fabi Date: Wed, 31 May 2023 16:40:25 +0200 Subject: [PATCH 255/502] Update bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index e24626a..e5a6a73 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,6 +1,6 @@ -name: 🐛 Bug Report -description: "Create a bug report to help us improve ZITADEL Terrafform Provider." -title: +name: Bug Report +description: "Create a bug report to help us improve ZITADEL. Click [here](https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#product-management) to see how we process your issue." +title: "[Bug]: " labels: ["bug"] body: - type: markdown @@ -21,13 +21,12 @@ body: id: version attributes: label: Version - required: true - description: Which version of the OIDC Library are you using. + description: Which version of ZITADEL are you using. - type: textarea id: impact attributes: - label: Describe the problems caused by this bug - description: A clear and concise description of what the problems you face and the bug you have. + label: Describe the problem caused by this bug + description: A clear and concise description of the problem you have and what the bug is. validations: required: true - type: textarea From e47f74932520165b250acbe4084f96eb2133c562 Mon Sep 17 00:00:00 2001 From: Fabi Date: Wed, 31 May 2023 16:41:24 +0200 Subject: [PATCH 256/502] Create improvement.yaml --- .github/ISSUE_TEMPLATE/improvement.yaml | 54 +++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/improvement.yaml diff --git a/.github/ISSUE_TEMPLATE/improvement.yaml b/.github/ISSUE_TEMPLATE/improvement.yaml new file mode 100644 index 0000000..2e2ddf4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/improvement.yaml @@ -0,0 +1,54 @@ +name: 🛠️ Improvement +description: "Create an new issue for an improvment in ZITADEL" +labels: ["improvement"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this improvement request + - type: checkboxes + id: preflight + attributes: + label: Preflight Checklist + options: + - label: + I could not find a solution in the existing issues, docs, nor discussions + required: true + - label: + I have joined the [ZITADEL chat](https://zitadel.com/chat) + - type: textarea + id: problem + attributes: + label: Describe your problem + description: Please describe your problem this improvement is supposed to solve. + placeholder: Describe the problem you have + validations: + required: true + - type: textarea + id: solution + attributes: + label: Describe your ideal solution + description: Which solution do you propose? + placeholder: As a [type of user], I want [some goal] so that [some reason]. + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Which version of the OIDC Library are you using. + - type: dropdown + id: environment + attributes: + label: Environment + description: How do you use ZITADEL? + options: + - ZITADEL Cloud + - Self-hosted + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional Context + description: Please add any other infos that could be useful. From e577bedd7f4e9e62abfd28a0cfbbfcd74d753310 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Jun 2023 12:06:53 +0200 Subject: [PATCH 257/502] chore(deps): bump github.com/sirupsen/logrus from 1.9.2 to 1.9.3 (#404) Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.9.2 to 1.9.3. - [Release notes](https://github.com/sirupsen/logrus/releases) - [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md) - [Commits](https://github.com/sirupsen/logrus/compare/v1.9.2...v1.9.3) --- updated-dependencies: - dependency-name: github.com/sirupsen/logrus dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 91028bc..6043f09 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/jeremija/gosubmit v0.2.7 github.com/muhlemmer/gu v0.3.1 github.com/rs/cors v1.9.0 - github.com/sirupsen/logrus v1.9.2 + github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.3 golang.org/x/oauth2 v0.8.0 golang.org/x/text v0.9.0 diff --git a/go.sum b/go.sum index 6919056..a34e4b3 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= -github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= From 77436a2ce74232ff5d889591099b9c4525dbd432 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Jun 2023 12:09:18 +0200 Subject: [PATCH 258/502] chore(deps): bump github.com/stretchr/testify from 1.8.3 to 1.8.4 (#401) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.3 to 1.8.4. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.8.3...v1.8.4) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6043f09..1476455 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/muhlemmer/gu v0.3.1 github.com/rs/cors v1.9.0 github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.8.3 + github.com/stretchr/testify v1.8.4 golang.org/x/oauth2 v0.8.0 golang.org/x/text v0.9.0 gopkg.in/square/go-jose.v2 v2.6.0 diff --git a/go.sum b/go.sum index a34e4b3..5975022 100644 --- a/go.sum +++ b/go.sum @@ -42,8 +42,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= From d01a5c8f9111f4c68306f309bd26055dddd23e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 9 Jun 2023 16:31:44 +0200 Subject: [PATCH 259/502] fix: don't error on invalid i18n tags in discovery (#407) * reproduce #406 * fix: don't error on invalid i18n tags in discovery This changes the use of `[]language.Tag` to `oidc.Locales` in `DiscoveryConfig`. This should be compatible with callers that use the `[]language.Tag` . Locales now implements the `json.Unmarshaler` interface. With support for json arrays or space seperated strings. The latter because `UnmarshalText` might have been implicetely called by the json library before we added UnmarshalJSON. Fixes: #406 --- pkg/client/client_test.go | 54 ++++++++++++++++++++++++++ pkg/oidc/discovery.go | 8 +--- pkg/oidc/types.go | 51 ++++++++++++++++++++++-- pkg/oidc/types_test.go | 81 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 185 insertions(+), 9 deletions(-) create mode 100644 pkg/client/client_test.go diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go new file mode 100644 index 0000000..02c408b --- /dev/null +++ b/pkg/client/client_test.go @@ -0,0 +1,54 @@ +package client + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDiscover(t *testing.T) { + type wantFields struct { + UILocalesSupported bool + } + + type args struct { + issuer string + wellKnownUrl []string + } + tests := []struct { + name string + args args + wantFields *wantFields + wantErr bool + }{ + { + name: "spotify", // https://github.com/zitadel/oidc/issues/406 + args: args{ + issuer: "https://accounts.spotify.com", + }, + wantFields: &wantFields{ + UILocalesSupported: true, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Discover(tt.args.issuer, http.DefaultClient, tt.args.wellKnownUrl...) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + if tt.wantFields == nil { + return + } + assert.Equal(t, tt.args.issuer, got.Issuer) + if tt.wantFields.UILocalesSupported { + assert.NotEmpty(t, got.UILocalesSupported) + } + }) + } +} diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go index 3574101..14fce5e 100644 --- a/pkg/oidc/discovery.go +++ b/pkg/oidc/discovery.go @@ -1,9 +1,5 @@ package oidc -import ( - "golang.org/x/text/language" -) - const ( DiscoveryEndpoint = "/.well-known/openid-configuration" ) @@ -130,10 +126,10 @@ type DiscoveryConfiguration struct { ServiceDocumentation string `json:"service_documentation,omitempty"` // ClaimsLocalesSupported contains a list of BCP47 language tag values that the OP supports for values of Claims returned. - ClaimsLocalesSupported []language.Tag `json:"claims_locales_supported,omitempty"` + ClaimsLocalesSupported Locales `json:"claims_locales_supported,omitempty"` // UILocalesSupported contains a list of BCP47 language tag values that the OP supports for the user interface. - UILocalesSupported []language.Tag `json:"ui_locales_supported,omitempty"` + UILocalesSupported Locales `json:"ui_locales_supported,omitempty"` // RequestParameterSupported specifies whether the OP supports use of the `request` parameter. If omitted, the default value is false. RequestParameterSupported bool `json:"request_parameter_supported,omitempty"` diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index 167f8b7..23367ef 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -9,6 +9,7 @@ import ( "time" "github.com/gorilla/schema" + "github.com/muhlemmer/gu" "golang.org/x/text/language" "gopkg.in/square/go-jose.v2" ) @@ -81,14 +82,58 @@ func (l *Locale) UnmarshalJSON(data []byte) error { type Locales []language.Tag -func (l *Locales) UnmarshalText(text []byte) error { - locales := strings.Split(string(text), " ") +// ParseLocales parses a slice of strings into Locales. +// If an entry causes a parse error or is undefined, +// it is ignored and not set to Locales. +func ParseLocales(locales []string) Locales { + out := make(Locales, 0, len(locales)) for _, locale := range locales { tag, err := language.Parse(locale) if err == nil && !tag.IsRoot() { - *l = append(*l, tag) + out = append(out, tag) } } + return out +} + +// UnmarshalText implements the [encoding.TextUnmarshaler] interface. +// It decodes an unquoted space seperated string into Locales. +// Undefined language tags in the input are ignored and ommited from +// the resulting Locales. +func (l *Locales) UnmarshalText(text []byte) error { + *l = ParseLocales( + strings.Split(string(text), " "), + ) + return nil +} + +// UnmarshalJSON implements the [json.Unmarshaler] interface. +// It decodes a json array or a space seperated string into Locales. +// Undefined language tags in the input are ignored and ommited from +// the resulting Locales. +func (l *Locales) UnmarshalJSON(data []byte) error { + var dst any + if err := json.Unmarshal(data, &dst); err != nil { + return fmt.Errorf("oidc locales: %w", err) + } + + // We catch the posibility of a space seperated string here, + // because UnmarshalText might have been implicetely called + // by the json library before we added UnmarshalJSON. + switch v := dst.(type) { + case nil: + *l = nil + case string: + *l = ParseLocales(strings.Split(v, " ")) + case []any: + locales, err := gu.AssertInterfaces[string](v) + if err != nil { + return fmt.Errorf("oidc locales: %w", err) + } + *l = ParseLocales(locales) + default: + return fmt.Errorf("oidc locales: unsupported type: %T", v) + } return nil } diff --git a/pkg/oidc/types_test.go b/pkg/oidc/types_test.go index 64f07f1..69540c2 100644 --- a/pkg/oidc/types_test.go +++ b/pkg/oidc/types_test.go @@ -224,6 +224,13 @@ func TestLocale_UnmarshalJSON(t *testing.T) { assert.Equal(t, want, got) } +func TestParseLocales(t *testing.T) { + in := []string{language.Afrikaans.String(), language.Danish.String(), "foobar", language.Und.String()} + want := Locales{language.Afrikaans, language.Danish} + got := ParseLocales(in) + assert.ElementsMatch(t, want, got) +} + func TestLocales_UnmarshalText(t *testing.T) { type args struct { text []byte @@ -281,6 +288,80 @@ func TestLocales_UnmarshalText(t *testing.T) { } } +func TestLocales_UnmarshalJSON(t *testing.T) { + in := []string{language.Afrikaans.String(), language.Danish.String(), "foobar", language.Und.String()} + spaceSepStr := strconv.Quote(strings.Join(in, " ")) + jsonArray, err := json.Marshal(in) + require.NoError(t, err) + + out := Locales{language.Afrikaans, language.Danish} + + type args struct { + data []byte + } + tests := []struct { + name string + args args + want Locales + wantErr bool + }{ + { + name: "invalid JSON", + args: args{ + data: []byte("~~~"), + }, + wantErr: true, + }, + { + name: "null", + args: args{ + data: []byte("null"), + }, + want: nil, + }, + { + name: "space seperated string", + args: args{ + data: []byte(spaceSepStr), + }, + want: out, + }, + { + name: "json string array", + args: args{ + data: jsonArray, + }, + want: out, + }, + { + name: "json invalid array", + args: args{ + data: []byte(`[1,2,3]`), + }, + wantErr: true, + }, + { + name: "invalid type (float64)", + args: args{ + data: []byte("22"), + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got Locales + err := got.UnmarshalJSON([]byte(tt.args.data)) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + func TestScopes_UnmarshalText(t *testing.T) { type args struct { text []byte From 148ed42ceeace94c1f8e9d62f97f7df8edb6afad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jun 2023 09:26:49 +0200 Subject: [PATCH 260/502] chore(deps): bump golang.org/x/text from 0.9.0 to 0.10.0 (#410) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.9.0 to 0.10.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.9.0...v0.10.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1476455..7e34259 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 golang.org/x/oauth2 v0.8.0 - golang.org/x/text v0.9.0 + golang.org/x/text v0.10.0 gopkg.in/square/go-jose.v2 v2.6.0 ) diff --git a/go.sum b/go.sum index 5975022..256fed6 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= From 9e624986aaf6eb8854ffc4702d7d1cc755919ff5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Jun 2023 13:55:31 +0200 Subject: [PATCH 261/502] chore(deps): bump golang.org/x/oauth2 from 0.8.0 to 0.9.0 (#411) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.8.0 to 0.9.0. - [Commits](https://github.com/golang/oauth2/compare/v0.8.0...v0.9.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 7e34259..1366131 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/rs/cors v1.9.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 - golang.org/x/oauth2 v0.8.0 + golang.org/x/oauth2 v0.9.0 golang.org/x/text v0.10.0 gopkg.in/square/go-jose.v2 v2.6.0 ) @@ -25,9 +25,9 @@ require ( github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.7.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect + golang.org/x/crypto v0.10.0 // indirect + golang.org/x/net v0.11.0 // indirect + golang.org/x/sys v0.9.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.29.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index 256fed6..32e4fc0 100644 --- a/go.sum +++ b/go.sum @@ -47,8 +47,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= +golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -56,11 +56,11 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= +golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= -golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= +golang.org/x/oauth2 v0.9.0 h1:BPpt2kU7oMRq3kCHAA1tbSEshXRw1LpG2ztgDwrzuAs= +golang.org/x/oauth2 v0.9.0/go.mod h1:qYgFZaFiu6Wg24azG8bdV52QJXJGbZzIIsRCdVKzbLw= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -69,8 +69,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= From 80c67e41271667cca1ebec9ee1cbdb7aec75503e Mon Sep 17 00:00:00 2001 From: Fabi Date: Wed, 21 Jun 2023 11:35:05 +0200 Subject: [PATCH 262/502] Update .github/ISSUE_TEMPLATE/bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index e5a6a73..92465f9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -21,7 +21,7 @@ body: id: version attributes: label: Version - description: Which version of ZITADEL are you using. + description: Which version of the OIDC library are you using. - type: textarea id: impact attributes: From 406153a4f40bd8700d78a7b5e158a15189045f3b Mon Sep 17 00:00:00 2001 From: Hugo Hromic Date: Fri, 23 Jun 2023 08:19:58 +0100 Subject: [PATCH 263/502] fix(client/rs): do not error when issuer discovery has no introspection endpoint (#414) * chore(tests): add basic unit tests for `pkg/client/rs/resource_server.go` * fix: do not error when issuer discovery has no introspection endpoint --- pkg/client/rs/resource_server.go | 15 +- pkg/client/rs/resource_server_test.go | 219 ++++++++++++++++++++++++++ 2 files changed, 230 insertions(+), 4 deletions(-) create mode 100644 pkg/client/rs/resource_server_test.go diff --git a/pkg/client/rs/resource_server.go b/pkg/client/rs/resource_server.go index 4e0353c..c641940 100644 --- a/pkg/client/rs/resource_server.go +++ b/pkg/client/rs/resource_server.go @@ -77,11 +77,15 @@ func newResourceServer(issuer string, authorizer func() (interface{}, error), op if err != nil { return nil, err } - rs.tokenURL = config.TokenEndpoint - rs.introspectURL = config.IntrospectionEndpoint + if rs.tokenURL == "" { + rs.tokenURL = config.TokenEndpoint + } + if rs.introspectURL == "" { + rs.introspectURL = config.IntrospectionEndpoint + } } - if rs.introspectURL == "" || rs.tokenURL == "" { - return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url") + if rs.tokenURL == "" { + return nil, errors.New("tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url") } rs.authFn = authorizer return rs, nil @@ -113,6 +117,9 @@ func WithStaticEndpoints(tokenURL, introspectURL string) Option { } func Introspect(ctx context.Context, rp ResourceServer, token string) (*oidc.IntrospectionResponse, error) { + if rp.IntrospectionURL() == "" { + return nil, errors.New("resource server: introspection URL is empty") + } authFn, err := rp.AuthFn() if err != nil { return nil, err diff --git a/pkg/client/rs/resource_server_test.go b/pkg/client/rs/resource_server_test.go new file mode 100644 index 0000000..b5fb496 --- /dev/null +++ b/pkg/client/rs/resource_server_test.go @@ -0,0 +1,219 @@ +package rs + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewResourceServer(t *testing.T) { + type args struct { + issuer string + authorizer func() (interface{}, error) + options []Option + } + type wantFields struct { + issuer string + tokenURL string + introspectURL string + authFn func() (interface{}, error) + } + tests := []struct { + name string + args args + wantFields *wantFields + wantErr bool + }{ + { + name: "spotify-full-discovery", + args: args{ + issuer: "https://accounts.spotify.com", + authorizer: nil, + options: []Option{}, + }, + wantFields: &wantFields{ + issuer: "https://accounts.spotify.com", + tokenURL: "https://accounts.spotify.com/api/token", + introspectURL: "", + authFn: nil, + }, + wantErr: false, + }, + { + name: "spotify-with-static-tokenurl", + args: args{ + issuer: "https://accounts.spotify.com", + authorizer: nil, + options: []Option{ + WithStaticEndpoints( + "https://some.host/token-url", + "", + ), + }, + }, + wantFields: &wantFields{ + issuer: "https://accounts.spotify.com", + tokenURL: "https://some.host/token-url", + introspectURL: "", + authFn: nil, + }, + wantErr: false, + }, + { + name: "spotify-with-static-introspecturl", + args: args{ + issuer: "https://accounts.spotify.com", + authorizer: nil, + options: []Option{ + WithStaticEndpoints( + "", + "https://some.host/instrospect-url", + ), + }, + }, + wantFields: &wantFields{ + issuer: "https://accounts.spotify.com", + tokenURL: "https://accounts.spotify.com/api/token", + introspectURL: "https://some.host/instrospect-url", + authFn: nil, + }, + wantErr: false, + }, + { + name: "spotify-with-all-static-endpoints", + args: args{ + issuer: "https://accounts.spotify.com", + authorizer: nil, + options: []Option{ + WithStaticEndpoints( + "https://some.host/token-url", + "https://some.host/instrospect-url", + ), + }, + }, + wantFields: &wantFields{ + issuer: "https://accounts.spotify.com", + tokenURL: "https://some.host/token-url", + introspectURL: "https://some.host/instrospect-url", + authFn: nil, + }, + wantErr: false, + }, + { + name: "bad-discovery", + args: args{ + issuer: "https://127.0.0.1:65535", + authorizer: nil, + options: []Option{}, + }, + wantFields: nil, + wantErr: true, + }, + { + name: "bad-discovery-with-static-tokenurl", + args: args{ + issuer: "https://127.0.0.1:65535", + authorizer: nil, + options: []Option{ + WithStaticEndpoints( + "https://some.host/token-url", + "", + ), + }, + }, + wantFields: nil, + wantErr: true, + }, + { + name: "bad-discovery-with-static-introspecturl", + args: args{ + issuer: "https://127.0.0.1:65535", + authorizer: nil, + options: []Option{ + WithStaticEndpoints( + "", + "https://some.host/instrospect-url", + ), + }, + }, + wantFields: nil, + wantErr: true, + }, + { + name: "bad-discovery-with-all-static-endpoints", + args: args{ + issuer: "https://127.0.0.1:65535", + authorizer: nil, + options: []Option{ + WithStaticEndpoints( + "https://some.host/token-url", + "https://some.host/instrospect-url", + ), + }, + }, + wantFields: &wantFields{ + issuer: "https://127.0.0.1:65535", + tokenURL: "https://some.host/token-url", + introspectURL: "https://some.host/instrospect-url", + authFn: nil, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := newResourceServer(tt.args.issuer, tt.args.authorizer, tt.args.options...) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + if tt.wantFields == nil { + return + } + assert.Equal(t, tt.wantFields.issuer, got.issuer) + assert.Equal(t, tt.wantFields.tokenURL, got.tokenURL) + assert.Equal(t, tt.wantFields.introspectURL, got.introspectURL) + }) + } +} + +func TestIntrospect(t *testing.T) { + type args struct { + ctx context.Context + rp ResourceServer + token string + } + rp, err := newResourceServer( + "https://accounts.spotify.com", + nil, + ) + require.NoError(t, err) + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "missing-introspect-url", + args: args{ + ctx: nil, + rp: rp, + token: "my-token", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := Introspect(tt.args.ctx, tt.args.rp, tt.args.token) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} From de5f4fbf3abdc71db744a15b99fbff869daf81f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Jul 2023 09:09:54 +0200 Subject: [PATCH 264/502] chore(deps): bump golang.org/x/text from 0.10.0 to 0.11.0 (#416) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.10.0 to 0.11.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.10.0...v0.11.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1366131..feacc89 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 golang.org/x/oauth2 v0.9.0 - golang.org/x/text v0.10.0 + golang.org/x/text v0.11.0 gopkg.in/square/go-jose.v2 v2.6.0 ) diff --git a/go.sum b/go.sum index 32e4fc0..1fa8a84 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= From 4c844da05ece7f30159b8ed5f3d77700fa3eac6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Jul 2023 07:11:36 +0000 Subject: [PATCH 265/502] chore(deps): bump golang.org/x/oauth2 from 0.9.0 to 0.10.0 (#417) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.9.0 to 0.10.0. - [Commits](https://github.com/golang/oauth2/compare/v0.9.0...v0.10.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 11 +++++------ go.sum | 21 ++++++++++----------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index feacc89..36574e7 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/rs/cors v1.9.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 - golang.org/x/oauth2 v0.9.0 + golang.org/x/oauth2 v0.10.0 golang.org/x/text v0.11.0 gopkg.in/square/go-jose.v2 v2.6.0 ) @@ -22,14 +22,13 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.10.0 // indirect - golang.org/x/net v0.11.0 // indirect - golang.org/x/sys v0.9.0 // indirect + golang.org/x/crypto v0.11.0 // indirect + golang.org/x/net v0.12.0 // indirect + golang.org/x/sys v0.10.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.29.1 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1fa8a84..20a9ddf 100644 --- a/go.sum +++ b/go.sum @@ -11,7 +11,6 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo= github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= @@ -47,8 +46,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= -golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -56,11 +55,11 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.9.0 h1:BPpt2kU7oMRq3kCHAA1tbSEshXRw1LpG2ztgDwrzuAs= -golang.org/x/oauth2 v0.9.0/go.mod h1:qYgFZaFiu6Wg24azG8bdV52QJXJGbZzIIsRCdVKzbLw= +golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= +golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -69,8 +68,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -89,8 +88,8 @@ google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM= -google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From be89c3b7bc0650e5dd7a92c40806698ce4df196c Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Tue, 18 Jul 2023 14:15:53 +0200 Subject: [PATCH 266/502] feat: add CanTerminateSessionFromRequest interface (#418) To support access to all claims in the id_token_hint (like a sessionID), this PR adds a new (optional) add-on interface to the Storage. --- pkg/op/session.go | 12 +++++++++--- pkg/op/storage.go | 15 ++++++++++++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/pkg/op/session.go b/pkg/op/session.go index c4f76f3..9014255 100644 --- a/pkg/op/session.go +++ b/pkg/op/session.go @@ -34,12 +34,17 @@ func EndSession(w http.ResponseWriter, r *http.Request, ender SessionEnder) { RequestError(w, r, err) return } - err = ender.Storage().TerminateSession(r.Context(), session.UserID, session.ClientID) + redirect := session.RedirectURI + if fromRequest, ok := ender.Storage().(CanTerminateSessionFromRequest); ok { + redirect, err = fromRequest.TerminateSessionFromRequest(r.Context(), session) + } else { + err = ender.Storage().TerminateSession(r.Context(), session.UserID, session.ClientID) + } if err != nil { RequestError(w, r, oidc.DefaultToServerError(err, "error terminating session")) return } - http.Redirect(w, r, session.RedirectURI, http.StatusFound) + http.Redirect(w, r, redirect, http.StatusFound) } func ParseEndSessionRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.EndSessionRequest, error) { @@ -60,11 +65,12 @@ func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest, RedirectURI: ender.DefaultLogoutRedirectURI(), } if req.IdTokenHint != "" { - claims, err := VerifyIDTokenHint[*oidc.TokenClaims](ctx, req.IdTokenHint, ender.IDTokenHintVerifier(ctx)) + claims, err := VerifyIDTokenHint[*oidc.IDTokenClaims](ctx, req.IdTokenHint, ender.IDTokenHintVerifier(ctx)) if err != nil { return nil, oidc.ErrInvalidRequest().WithDescription("id_token_hint invalid").WithParent(err) } session.UserID = claims.GetSubject() + session.IDTokenHintClaims = claims if req.ClientID != "" && req.ClientID != claims.GetAuthorizedParty() { return nil, oidc.ErrInvalidRequest().WithDescription("client_id does not match azp of id_token_hint") } diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 590c4a0..72b75e0 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -62,6 +62,14 @@ type AuthStorage interface { KeySet(context.Context) ([]Key, error) } +// CanTerminateSessionFromRequest is an optional additional interface that may be implemented by +// implementors of Storage as an alternative to TerminateSession of the AuthStorage. +// It passes the complete parsed EndSessionRequest to the implementation, which allows access to additional data. +// It also allows to modify the uri, which will be used for redirection, (e.g. a UI where the user can consent to the logout) +type CanTerminateSessionFromRequest interface { + TerminateSessionFromRequest(ctx context.Context, endSessionRequest *EndSessionRequest) (string, error) +} + type ClientCredentialsStorage interface { ClientCredentials(ctx context.Context, clientID, clientSecret string) (Client, error) ClientCredentialsTokenRequest(ctx context.Context, clientID string, scopes []string) (TokenRequest, error) @@ -152,9 +160,10 @@ type StorageNotFoundError interface { } type EndSessionRequest struct { - UserID string - ClientID string - RedirectURI string + UserID string + ClientID string + IDTokenHintClaims *oidc.IDTokenClaims + RedirectURI string } var ErrDuplicateUserCode = errors.New("user code already exists") From 9a483321ab0d78cd0c66d6039388b697de09dfc5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Aug 2023 13:01:43 +0300 Subject: [PATCH 267/502] chore(deps): bump golang.org/x/text from 0.11.0 to 0.12.0 (#422) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.11.0 to 0.12.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.11.0...v0.12.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 36574e7..b212793 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 golang.org/x/oauth2 v0.10.0 - golang.org/x/text v0.11.0 + golang.org/x/text v0.12.0 gopkg.in/square/go-jose.v2 v2.6.0 ) diff --git a/go.sum b/go.sum index 20a9ddf..82ddb1d 100644 --- a/go.sum +++ b/go.sum @@ -74,8 +74,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= From 48a5fdb8a61112a0e848d31b309c289c5c849123 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Aug 2023 10:03:30 +0000 Subject: [PATCH 268/502] chore(deps): bump golang.org/x/oauth2 from 0.10.0 to 0.11.0 (#421) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.10.0 to 0.11.0. - [Commits](https://github.com/golang/oauth2/compare/v0.10.0...v0.11.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index b212793..4b438d9 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/rs/cors v1.9.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 - golang.org/x/oauth2 v0.10.0 + golang.org/x/oauth2 v0.11.0 golang.org/x/text v0.12.0 gopkg.in/square/go-jose.v2 v2.6.0 ) @@ -24,9 +24,9 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.11.0 // indirect - golang.org/x/net v0.12.0 // indirect - golang.org/x/sys v0.10.0 // indirect + golang.org/x/crypto v0.12.0 // indirect + golang.org/x/net v0.14.0 // indirect + golang.org/x/sys v0.11.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index 82ddb1d..60fb564 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -55,11 +55,11 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= -golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= +golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= +golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -68,8 +68,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= From 45582b6ee9b11f32cbfa07cc6c0a3a67b700a06d Mon Sep 17 00:00:00 2001 From: Diego Parisi Date: Mon, 14 Aug 2023 17:14:24 +0200 Subject: [PATCH 269/502] feat: delete PKCE cookie after code exchange (#419) --- pkg/client/rp/relying_party.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 114599d..051b8c8 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -438,6 +438,7 @@ func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp R return } codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier)) + rp.CookieHandler().DeleteCookie(w, pkceCode) } if rp.Signer() != nil { assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer()}, time.Hour, rp.Signer()) From 6708ef4c247e6583abe54750427e93c70aaa6c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 18 Aug 2023 15:36:39 +0300 Subject: [PATCH 270/502] feat(rp): return oidc.Tokens on token refresh (#423) BREAKING CHANGE: - rename RefreshAccessToken to RefreshToken - RefreshToken returns *oidc.Tokens instead of *oauth2.Token This change allows the return of the id_token in an explicit manner, as part of the oidc.Tokens struct. The return type is now consistent with the CodeExchange function. When an id_token is returned, it is verified. In case no id_token was received, RefreshTokens will not return an error. As per specifictation: https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse Upon successful validation of the Refresh Token, the response body is the Token Response of Section 3.1.3.3 except that it might not contain an id_token. Closes #364 --- pkg/client/integration_test.go | 43 ++++++----- pkg/client/rp/relying_party.go | 59 +++++++++------ pkg/client/rp/relying_party_test.go | 107 ++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 43 deletions(-) create mode 100644 pkg/client/rp/relying_party_test.go diff --git a/pkg/client/integration_test.go b/pkg/client/integration_test.go index d8b3f25..073efef 100644 --- a/pkg/client/integration_test.go +++ b/pkg/client/integration_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "io" - "io/ioutil" "math/rand" "net/http" "net/http/cookiejar" @@ -56,11 +55,11 @@ func TestRelyingPartySession(t *testing.T) { clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25) t.Log("------- run authorization code flow ------") - provider, _, refreshToken, idToken := RunAuthorizationCodeFlow(t, opServer, clientID, "secret") + provider, tokens := RunAuthorizationCodeFlow(t, opServer, clientID, "secret") t.Log("------- refresh tokens ------") - newTokens, err := rp.RefreshAccessToken(CTX, provider, refreshToken, "", "") + newTokens, err := rp.RefreshTokens[*oidc.IDTokenClaims](CTX, provider, tokens.RefreshToken, "", "") require.NoError(t, err, "refresh token") assert.NotNil(t, newTokens, "access token") t.Logf("new access token %s", newTokens.AccessToken) @@ -68,11 +67,13 @@ func TestRelyingPartySession(t *testing.T) { t.Logf("new token type %s", newTokens.TokenType) t.Logf("new expiry %s", newTokens.Expiry.Format(time.RFC3339)) require.NotEmpty(t, newTokens.AccessToken, "new accessToken") - assert.NotEmpty(t, newTokens.Extra("id_token"), "new idToken") + assert.NotEmpty(t, newTokens.IDToken, "new idToken") + assert.NotNil(t, newTokens.IDTokenClaims) + assert.Equal(t, newTokens.IDTokenClaims.Subject, tokens.IDTokenClaims.Subject) t.Log("------ end session (logout) ------") - newLoc, err := rp.EndSession(CTX, provider, idToken, "", "") + newLoc, err := rp.EndSession(CTX, provider, tokens.IDToken, "", "") require.NoError(t, err, "logout") if newLoc != nil { t.Logf("redirect to %s", newLoc) @@ -81,12 +82,12 @@ func TestRelyingPartySession(t *testing.T) { } t.Log("------ attempt refresh again (should fail) ------") - t.Log("trying original refresh token", refreshToken) - _, err = rp.RefreshAccessToken(CTX, provider, refreshToken, "", "") + t.Log("trying original refresh token", tokens.RefreshToken) + _, err = rp.RefreshTokens[*oidc.IDTokenClaims](CTX, provider, tokens.RefreshToken, "", "") assert.Errorf(t, err, "refresh with original") if newTokens.RefreshToken != "" { t.Log("trying replacement refresh token", newTokens.RefreshToken) - _, err = rp.RefreshAccessToken(CTX, provider, newTokens.RefreshToken, "", "") + _, err = rp.RefreshTokens[*oidc.IDTokenClaims](CTX, provider, newTokens.RefreshToken, "", "") assert.Errorf(t, err, "refresh with replacement") } } @@ -106,7 +107,7 @@ func TestResourceServerTokenExchange(t *testing.T) { clientSecret := "secret" t.Log("------- run authorization code flow ------") - provider, _, refreshToken, idToken := RunAuthorizationCodeFlow(t, opServer, clientID, clientSecret) + provider, tokens := RunAuthorizationCodeFlow(t, opServer, clientID, clientSecret) resourceServer, err := rs.NewResourceServerClientCredentials(CTX, opServer.URL, clientID, clientSecret) require.NoError(t, err, "new resource server") @@ -116,7 +117,7 @@ func TestResourceServerTokenExchange(t *testing.T) { tokenExchangeResponse, err := tokenexchange.ExchangeToken( CTX, resourceServer, - refreshToken, + tokens.RefreshToken, oidc.RefreshTokenType, "", "", @@ -134,7 +135,7 @@ func TestResourceServerTokenExchange(t *testing.T) { t.Log("------ end session (logout) ------") - newLoc, err := rp.EndSession(CTX, provider, idToken, "", "") + newLoc, err := rp.EndSession(CTX, provider, tokens.IDToken, "", "") require.NoError(t, err, "logout") if newLoc != nil { t.Logf("redirect to %s", newLoc) @@ -147,7 +148,7 @@ func TestResourceServerTokenExchange(t *testing.T) { tokenExchangeResponse, err = tokenexchange.ExchangeToken( CTX, resourceServer, - refreshToken, + tokens.RefreshToken, oidc.RefreshTokenType, "", "", @@ -161,7 +162,7 @@ func TestResourceServerTokenExchange(t *testing.T) { require.Nil(t, tokenExchangeResponse, "token exchange response") } -func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID, clientSecret string) (provider rp.RelyingParty, accessToken, refreshToken, idToken string) { +func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID, clientSecret string) (provider rp.RelyingParty, tokens *oidc.Tokens[*oidc.IDTokenClaims]) { targetURL := "http://local-site" localURL, err := url.Parse(targetURL + "/login?requestID=1234") require.NoError(t, err, "local url") @@ -258,7 +259,8 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID, } var email string - redirect := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) { + redirect := func(w http.ResponseWriter, r *http.Request, newTokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) { + tokens = newTokens require.NotNil(t, tokens, "tokens") require.NotNil(t, info, "info") t.Log("access token", tokens.AccessToken) @@ -266,9 +268,6 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID, t.Log("id token", tokens.IDToken) t.Log("email", info.Email) - accessToken = tokens.AccessToken - refreshToken = tokens.RefreshToken - idToken = tokens.IDToken email = info.Email http.Redirect(w, r, targetURL, 302) } @@ -290,12 +289,12 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID, require.NoError(t, err, "get fully-authorizied redirect location") require.Equal(t, targetURL, authorizedURL.String(), "fully-authorizied redirect location") - require.NotEmpty(t, idToken, "id token") - assert.NotEmpty(t, refreshToken, "refresh token") - assert.NotEmpty(t, accessToken, "access token") + require.NotEmpty(t, tokens.IDToken, "id token") + assert.NotEmpty(t, tokens.RefreshToken, "refresh token") + assert.NotEmpty(t, tokens.AccessToken, "access token") assert.NotEmpty(t, email, "email") - return provider, accessToken, refreshToken, idToken + return provider, tokens } type deferredHandler struct { @@ -343,7 +342,7 @@ func getForm(t *testing.T, desc string, httpClient *http.Client, uri *url.URL) [ func fillForm(t *testing.T, desc string, httpClient *http.Client, body []byte, uri *url.URL, opts ...gosubmit.Option) *url.URL { // TODO: switch to io.NopCloser when go1.15 support is dropped - req := gosubmit.ParseWithURL(ioutil.NopCloser(bytes.NewReader(body)), uri.String()).FirstForm().Testing(t).NewTestRequest( + req := gosubmit.ParseWithURL(io.NopCloser(bytes.NewReader(body)), uri.String()).FirstForm().Testing(t).NewTestRequest( append([]gosubmit.Option{gosubmit.AutoFill()}, opts...)..., ) if req.URL.Scheme == "" { diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 7d73a5a..5597c9d 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -356,6 +356,25 @@ func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (stri return oidc.NewSHACodeChallenge(codeVerifier), nil } +// ErrMissingIDToken is returned when an id_token was expected, +// but not received in the token response. +var ErrMissingIDToken = errors.New("id_token missing") + +func verifyTokenResponse[C oidc.IDClaims](ctx context.Context, token *oauth2.Token, rp RelyingParty) (*oidc.Tokens[C], error) { + if rp.IsOAuth2Only() { + return &oidc.Tokens[C]{Token: token}, nil + } + idTokenString, ok := token.Extra(idTokenKey).(string) + if !ok { + return &oidc.Tokens[C]{Token: token}, ErrMissingIDToken + } + idToken, err := VerifyTokens[C](ctx, token.AccessToken, idTokenString, rp.IDTokenVerifier()) + if err != nil { + return nil, err + } + return &oidc.Tokens[C]{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil +} + // CodeExchange handles the oauth2 code exchange, extracting and validating the id_token // returning it parsed together with the oauth2 tokens (access, refresh) func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens[C], err error) { @@ -369,22 +388,7 @@ func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingP if err != nil { return nil, err } - - if rp.IsOAuth2Only() { - return &oidc.Tokens[C]{Token: token}, nil - } - - idTokenString, ok := token.Extra(idTokenKey).(string) - if !ok { - return nil, errors.New("id_token missing") - } - - idToken, err := VerifyTokens[C](ctx, token.AccessToken, idTokenString, rp.IDTokenVerifier()) - if err != nil { - return nil, err - } - - return &oidc.Tokens[C]{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil + return verifyTokenResponse[C](ctx, token, rp) } type CodeExchangeCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) @@ -609,11 +613,14 @@ type RefreshTokenRequest struct { GrantType oidc.GrantType `schema:"grant_type"` } -// RefreshAccessToken performs a token refresh. If it doesn't error, it will always +// RefreshTokens performs a token refresh. If it doesn't error, it will always // provide a new AccessToken. It may provide a new RefreshToken, and if it does, then -// the old one should be considered invalid. It may also provide a new IDToken. The -// new IDToken can be retrieved with token.Extra("id_token"). -func RefreshAccessToken(ctx context.Context, rp RelyingParty, refreshToken, clientAssertion, clientAssertionType string) (*oauth2.Token, error) { +// the old one should be considered invalid. +// +// In case the RP is not OAuth2 only and an IDToken was part of the response, +// the IDToken and AccessToken will be verfied +// and the IDToken and IDTokenClaims fields will be populated in the returned object. +func RefreshTokens[C oidc.IDClaims](ctx context.Context, rp RelyingParty, refreshToken, clientAssertion, clientAssertionType string) (*oidc.Tokens[C], error) { request := RefreshTokenRequest{ RefreshToken: refreshToken, Scopes: rp.OAuthConfig().Scopes, @@ -623,7 +630,17 @@ func RefreshAccessToken(ctx context.Context, rp RelyingParty, refreshToken, clie ClientAssertionType: clientAssertionType, GrantType: oidc.GrantTypeRefreshToken, } - return client.CallTokenEndpoint(ctx, request, tokenEndpointCaller{RelyingParty: rp}) + newToken, err := client.CallTokenEndpoint(ctx, request, tokenEndpointCaller{RelyingParty: rp}) + if err != nil { + return nil, err + } + tokens, err := verifyTokenResponse[C](ctx, newToken, rp) + if err == nil || errors.Is(err, ErrMissingIDToken) { + // https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse + // ...except that it might not contain an id_token. + return tokens, nil + } + return nil, err } func EndSession(ctx context.Context, rp RelyingParty, idToken, optionalRedirectURI, optionalState string) (*url.URL, error) { diff --git a/pkg/client/rp/relying_party_test.go b/pkg/client/rp/relying_party_test.go new file mode 100644 index 0000000..4c5a1b3 --- /dev/null +++ b/pkg/client/rp/relying_party_test.go @@ -0,0 +1,107 @@ +package rp + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + tu "github.com/zitadel/oidc/v3/internal/testutil" + "github.com/zitadel/oidc/v3/pkg/oidc" + "golang.org/x/oauth2" +) + +func Test_verifyTokenResponse(t *testing.T) { + verifier := &IDTokenVerifier{ + Issuer: tu.ValidIssuer, + MaxAgeIAT: 2 * time.Minute, + ClientID: tu.ValidClientID, + Offset: time.Second, + SupportedSignAlgs: []string{string(tu.SignatureAlgorithm)}, + KeySet: tu.KeySet{}, + MaxAge: 2 * time.Minute, + ACR: tu.ACRVerify, + Nonce: func(context.Context) string { return tu.ValidNonce }, + } + tests := []struct { + name string + oauth2Only bool + tokens func() (token *oauth2.Token, want *oidc.Tokens[*oidc.IDTokenClaims]) + wantErr error + }{ + { + name: "succes, oauth2 only", + oauth2Only: true, + tokens: func() (*oauth2.Token, *oidc.Tokens[*oidc.IDTokenClaims]) { + accesToken, _ := tu.ValidAccessToken() + token := &oauth2.Token{ + AccessToken: accesToken, + } + return token, &oidc.Tokens[*oidc.IDTokenClaims]{ + Token: token, + } + }, + }, + { + name: "id_token missing error", + oauth2Only: false, + tokens: func() (*oauth2.Token, *oidc.Tokens[*oidc.IDTokenClaims]) { + accesToken, _ := tu.ValidAccessToken() + token := &oauth2.Token{ + AccessToken: accesToken, + } + return token, &oidc.Tokens[*oidc.IDTokenClaims]{ + Token: token, + } + }, + wantErr: ErrMissingIDToken, + }, + { + name: "verify tokens error", + oauth2Only: false, + tokens: func() (*oauth2.Token, *oidc.Tokens[*oidc.IDTokenClaims]) { + accesToken, _ := tu.ValidAccessToken() + token := &oauth2.Token{ + AccessToken: accesToken, + } + token = token.WithExtra(map[string]any{ + "id_token": "foobar", + }) + return token, nil + }, + wantErr: oidc.ErrParse, + }, + { + name: "success, with id_token", + oauth2Only: false, + tokens: func() (*oauth2.Token, *oidc.Tokens[*oidc.IDTokenClaims]) { + accesToken, _ := tu.ValidAccessToken() + token := &oauth2.Token{ + AccessToken: accesToken, + } + idToken, claims := tu.ValidIDToken() + token = token.WithExtra(map[string]any{ + "id_token": idToken, + }) + return token, &oidc.Tokens[*oidc.IDTokenClaims]{ + Token: token, + IDTokenClaims: claims, + IDToken: idToken, + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rp := &relyingParty{ + oauth2Only: tt.oauth2Only, + idTokenVerifier: verifier, + } + token, want := tt.tokens() + got, err := verifyTokenResponse[*oidc.IDTokenClaims](context.Background(), token, rp) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, want, got) + }) + } +} From 37b5de0e821cbaa29dd9cf56ef7f38fc77ae29d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 18 Aug 2023 16:03:51 +0300 Subject: [PATCH 271/502] fix(op): omit empty state from code flow redirect (#428) * chore(op): reproduce issue #415 * fix(op): omit empty state from code flow redirect Add test cases to reproduce the original bug, and it's resolution. closes #415 --- pkg/op/auth_request.go | 8 +-- pkg/op/auth_request_test.go | 131 +++++++++++++++++++++++++++++++++++- 2 files changed, 134 insertions(+), 5 deletions(-) diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index c264605..5621951 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -448,11 +448,11 @@ func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthReques return } codeResponse := struct { - code string - state string + Code string `schema:"code"` + State string `schema:"state,omitempty"` }{ - code: code, - state: authReq.GetState(), + Code: code, + State: authReq.GetState(), } callback, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), &codeResponse, authorizer.Encoder()) if err != nil { diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index 2bba4e7..1fadffc 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -3,6 +3,7 @@ package op_test import ( "context" "errors" + "io" "net/http" "net/http/httptest" "net/url" @@ -13,7 +14,7 @@ import ( "github.com/gorilla/schema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - + "github.com/zitadel/oidc/v2/example/server/storage" httphelper "github.com/zitadel/oidc/v2/pkg/http" "github.com/zitadel/oidc/v2/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/op" @@ -942,3 +943,131 @@ func (m *mockEncoder) Encode(src interface{}, dst map[string][]string) error { } return nil } + +// mockCrypto implements the op.Crypto interface +// and in always equals out. (It doesn't crypt anything). +// When returnErr != nil, that error is always returned instread. +type mockCrypto struct { + returnErr error +} + +func (c *mockCrypto) Encrypt(s string) (string, error) { + if c.returnErr != nil { + return "", c.returnErr + } + return s, nil +} + +func (c *mockCrypto) Decrypt(s string) (string, error) { + if c.returnErr != nil { + return "", c.returnErr + } + return s, nil +} + +func TestAuthResponseCode(t *testing.T) { + type args struct { + authReq op.AuthRequest + authorizer func(*testing.T) op.Authorizer + } + type res struct { + wantCode int + wantLocationHeader string + wantBody string + } + tests := []struct { + name string + args args + res res + }{ + { + name: "create code error", + args: args{ + authReq: &storage.AuthRequest{ + ID: "id1", + TransferState: "state1", + }, + authorizer: func(t *testing.T) op.Authorizer { + ctrl := gomock.NewController(t) + storage := mock.NewMockStorage(ctrl) + + authorizer := mock.NewMockAuthorizer(ctrl) + authorizer.EXPECT().Storage().Return(storage) + authorizer.EXPECT().Crypto().Return(&mockCrypto{ + returnErr: io.ErrClosedPipe, + }) + authorizer.EXPECT().Encoder().Return(schema.NewEncoder()) + return authorizer + }, + }, + res: res{ + wantCode: http.StatusBadRequest, + wantBody: "io: read/write on closed pipe\n", + }, + }, + { + name: "success with state", + args: args{ + authReq: &storage.AuthRequest{ + ID: "id1", + TransferState: "state1", + }, + authorizer: func(t *testing.T) op.Authorizer { + ctrl := gomock.NewController(t) + storage := mock.NewMockStorage(ctrl) + storage.EXPECT().SaveAuthCode(context.Background(), "id1", "id1") + + authorizer := mock.NewMockAuthorizer(ctrl) + authorizer.EXPECT().Storage().Return(storage) + authorizer.EXPECT().Crypto().Return(&mockCrypto{}) + authorizer.EXPECT().Encoder().Return(schema.NewEncoder()) + return authorizer + }, + }, + res: res{ + wantCode: http.StatusFound, + wantLocationHeader: "/auth/callback/?code=id1&state=state1", + wantBody: "", + }, + }, + { + name: "success without state", // reproduce issue #415 + args: args{ + authReq: &storage.AuthRequest{ + ID: "id1", + TransferState: "", + }, + authorizer: func(t *testing.T) op.Authorizer { + ctrl := gomock.NewController(t) + storage := mock.NewMockStorage(ctrl) + storage.EXPECT().SaveAuthCode(context.Background(), "id1", "id1") + + authorizer := mock.NewMockAuthorizer(ctrl) + authorizer.EXPECT().Storage().Return(storage) + authorizer.EXPECT().Crypto().Return(&mockCrypto{}) + authorizer.EXPECT().Encoder().Return(schema.NewEncoder()) + return authorizer + }, + }, + res: res{ + wantCode: http.StatusFound, + wantLocationHeader: "/auth/callback/?code=id1", + wantBody: "", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest(http.MethodPost, "/auth/callback/", nil) + w := httptest.NewRecorder() + op.AuthResponseCode(w, r, tt.args.authReq, tt.args.authorizer(t)) + resp := w.Result() + defer resp.Body.Close() + assert.Equal(t, tt.res.wantCode, resp.StatusCode) + assert.Equal(t, tt.res.wantLocationHeader, resp.Header.Get("Location")) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, tt.res.wantBody, string(body)) + }) + } +} From 4ed269979e119dd6f133f19bb4356b01919871ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 18 Aug 2023 18:54:58 +0300 Subject: [PATCH 272/502] fix(op): check if getTokenIDAndClaims succeeded (#429) When getTokenIDAndClaims didn't succeed, so `ok` would be false. This was ignored and the accessTokenClaims.Claims call would panic. --- pkg/op/token_exchange.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/op/token_exchange.go b/pkg/op/token_exchange.go index 055ff13..5a2387d 100644 --- a/pkg/op/token_exchange.go +++ b/pkg/op/token_exchange.go @@ -282,6 +282,9 @@ func GetTokenIDAndSubjectFromToken( case oidc.AccessTokenType: var accessTokenClaims *oidc.AccessTokenClaims tokenIDOrToken, subject, accessTokenClaims, ok = getTokenIDAndClaims(ctx, exchanger, token) + if !ok { + break + } claims = accessTokenClaims.Claims case oidc.RefreshTokenType: refreshTokenRequest, err := exchanger.Storage().TokenRequestByRefreshToken(ctx, token) From ce85a8b82037146eeec53b644a7c59277403fc53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Mon, 21 Aug 2023 07:44:33 +0200 Subject: [PATCH 273/502] fix(exampleop): pass the issuer interceptor to login (#430) * fix(exampleop): pass the issuer interceptor to login * undo example testing changes --- example/server/exampleop/login.go | 9 +++++---- example/server/exampleop/op.go | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/example/server/exampleop/login.go b/example/server/exampleop/login.go index c014c9a..64045b8 100644 --- a/example/server/exampleop/login.go +++ b/example/server/exampleop/login.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/gorilla/mux" + "github.com/zitadel/oidc/v2/pkg/op" ) type login struct { @@ -14,19 +15,19 @@ type login struct { callback func(context.Context, string) string } -func NewLogin(authenticate authenticate, callback func(context.Context, string) string) *login { +func NewLogin(authenticate authenticate, callback func(context.Context, string) string, issuerInterceptor *op.IssuerInterceptor) *login { l := &login{ authenticate: authenticate, callback: callback, } - l.createRouter() + l.createRouter(issuerInterceptor) return l } -func (l *login) createRouter() { +func (l *login) createRouter(issuerInterceptor *op.IssuerInterceptor) { l.router = mux.NewRouter() l.router.Path("/username").Methods("GET").HandlerFunc(l.loginHandler) - l.router.Path("/username").Methods("POST").HandlerFunc(l.checkLoginHandler) + l.router.Path("/username").Methods("POST").HandlerFunc(issuerInterceptor.HandlerFunc(l.checkLoginHandler)) } type authenticate interface { diff --git a/example/server/exampleop/op.go b/example/server/exampleop/op.go index 7254585..20190ca 100644 --- a/example/server/exampleop/op.go +++ b/example/server/exampleop/op.go @@ -55,9 +55,10 @@ func SetupServer(issuer string, storage Storage, extraOptions ...op.Option) *mux log.Fatal(err) } - // the provider will only take care of the OpenID Protocol, so there must be some sort of UI for the login process - // for the simplicity of the example this means a simple page with username and password field - l := NewLogin(storage, op.AuthCallbackURL(provider)) + //the provider will only take care of the OpenID Protocol, so there must be some sort of UI for the login process + //for the simplicity of the example this means a simple page with username and password field + //be sure to provide an IssuerInterceptor with the IssuerFromRequest from the OP so the login can select / and pass it to the storage + l := NewLogin(storage, op.AuthCallbackURL(provider), op.NewIssuerInterceptor(provider.IssuerFromRequest)) // regardless of how many pages / steps there are in the process, the UI must be registered in the router, // so we will direct all calls to /login to the login UI From d7e88060be4f283dfbb80cdb2d4cfd1d854919be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Aug 2023 18:29:03 +0200 Subject: [PATCH 274/502] chore(deps): bump github.com/google/uuid from 1.3.0 to 1.3.1 (#431) Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.3.0 to 1.3.1. - [Release notes](https://github.com/google/uuid/releases) - [Changelog](https://github.com/google/uuid/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/uuid/compare/v1.3.0...v1.3.1) --- updated-dependencies: - dependency-name: github.com/google/uuid dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4b438d9..6abc07b 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.3.1 github.com/gorilla/mux v1.8.0 github.com/gorilla/schema v1.2.0 github.com/gorilla/securecookie v1.1.1 diff --git a/go.sum b/go.sum index 60fb564..be1233b 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,8 @@ github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gA github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= From 0879c883996b646ef0d96faa98e142afcfc9cd08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 29 Aug 2023 15:07:45 +0300 Subject: [PATCH 275/502] feat: add slog logging (#432) * feat(op): user slog for logging integrate with golang.org/x/exp/slog for logging. provide a middleware for request scoped logging. BREAKING CHANGES: 1. OpenIDProvider and sub-interfaces get a Logger() method to return the configured logger; 2. AuthRequestError now takes the complete Authorizer, instead of only the encoder. So that it may use its Logger() method. 3. RequestError now takes a Logger as argument. * use zitadel/logging * finish op and testing without middleware for now * minimum go version 1.19 * update go mod * log value testing only on go 1.20 or later * finish the RP and example * ping logging release --- .github/workflows/release.yml | 2 +- README.md | 6 +- example/client/app/app.go | 44 ++++- example/server/exampleop/op.go | 26 ++- example/server/main.go | 19 +- example/server/storage/oidc.go | 14 ++ go.mod | 8 +- go.sum | 13 +- pkg/client/client.go | 5 + pkg/client/integration_test.go | 12 +- pkg/client/rp/device.go | 2 + pkg/client/rp/log.go | 17 ++ pkg/client/rp/relying_party.go | 31 +++- pkg/oidc/authorization.go | 13 ++ pkg/oidc/authorization_test.go | 27 +++ pkg/oidc/error.go | 33 ++++ pkg/oidc/error_go120_test.go | 83 +++++++++ pkg/oidc/error_test.go | 81 +++++++++ pkg/oidc/types.go | 8 +- pkg/op/auth_request.go | 34 ++-- pkg/op/auth_request_test.go | 3 +- pkg/op/device.go | 4 +- pkg/op/error.go | 32 +++- pkg/op/error_test.go | 277 +++++++++++++++++++++++++++++ pkg/op/mock/authorizer.mock.go | 15 ++ pkg/op/op.go | 20 +++ pkg/op/op_test.go | 10 +- pkg/op/session.go | 6 +- pkg/op/token_client_credentials.go | 6 +- pkg/op/token_code.go | 8 +- pkg/op/token_exchange.go | 6 +- pkg/op/token_jwt_profile.go | 8 +- pkg/op/token_refresh.go | 6 +- pkg/op/token_request.go | 6 +- 34 files changed, 800 insertions(+), 85 deletions(-) create mode 100644 pkg/client/rp/log.go create mode 100644 pkg/oidc/authorization_test.go create mode 100644 pkg/oidc/error_go120_test.go create mode 100644 pkg/oidc/error_test.go create mode 100644 pkg/op/error_test.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7483b2f..329428d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - go: ['1.18', '1.19', '1.20'] + go: ['1.19', '1.20', '1.21'] name: Go ${{ matrix.go }} test steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index b7993e6..91a2f39 100644 --- a/README.md +++ b/README.md @@ -115,10 +115,10 @@ Versions that also build are marked with :warning:. | Version | Supported | | ------- | ------------------ | -| <1.18 | :x: | -| 1.18 | :warning: | -| 1.19 | :white_check_mark: | +| <1.19 | :x: | +| 1.19 | :warning: | | 1.20 | :white_check_mark: | +| 1.21 | :white_check_mark: | ## Why another library diff --git a/example/client/app/app.go b/example/client/app/app.go index 2cb5dfa..0e339f4 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -7,11 +7,14 @@ import ( "net/http" "os" "strings" + "sync/atomic" "time" "github.com/google/uuid" "github.com/sirupsen/logrus" + "golang.org/x/exp/slog" + "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/client/rp" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" @@ -33,9 +36,25 @@ func main() { redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath) cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure()) + logger := slog.New( + slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelDebug, + }), + ) + client := &http.Client{ + Timeout: time.Minute, + } + // enable outgoing request logging + logging.EnableHTTPClient(client, + logging.WithClientGroup("client"), + ) + options := []rp.Option{ rp.WithCookieHandler(cookieHandler), rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)), + rp.WithHTTPClient(client), + rp.WithLogger(logger), } if clientSecret == "" { options = append(options, rp.WithPKCE(cookieHandler)) @@ -44,7 +63,10 @@ func main() { options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath))) } - provider, err := rp.NewRelyingPartyOIDC(context.TODO(), issuer, clientID, clientSecret, redirectURI, scopes, options...) + // One can add a logger to the context, + // pre-defining log attributes as required. + ctx := logging.ToContext(context.TODO(), logger) + provider, err := rp.NewRelyingPartyOIDC(ctx, issuer, clientID, clientSecret, redirectURI, scopes, options...) if err != nil { logrus.Fatalf("error creating provider %s", err.Error()) } @@ -119,8 +141,22 @@ func main() { // // http.Handle(callbackPath, rp.CodeExchangeHandler(marshalToken, provider)) + // simple counter for request IDs + var counter atomic.Int64 + // enable incomming request logging + mw := logging.Middleware( + logging.WithLogger(logger), + logging.WithGroup("server"), + logging.WithIDFunc(func() slog.Attr { + return slog.Int64("id", counter.Add(1)) + }), + ) + lis := fmt.Sprintf("127.0.0.1:%s", port) - logrus.Infof("listening on http://%s/", lis) - logrus.Info("press ctrl+c to stop") - logrus.Fatal(http.ListenAndServe(lis, nil)) + logger.Info("server listening, press ctrl+c to stop", "addr", lis) + err = http.ListenAndServe(lis, mw(http.DefaultServeMux)) + if err != http.ErrServerClosed { + logger.Error("server terminated", "error", err) + os.Exit(1) + } } diff --git a/example/server/exampleop/op.go b/example/server/exampleop/op.go index 298bff6..b5ee7b3 100644 --- a/example/server/exampleop/op.go +++ b/example/server/exampleop/op.go @@ -4,9 +4,12 @@ import ( "crypto/sha256" "log" "net/http" + "sync/atomic" "time" "github.com/go-chi/chi" + "github.com/zitadel/logging" + "golang.org/x/exp/slog" "golang.org/x/text/language" "github.com/zitadel/oidc/v3/example/server/storage" @@ -31,26 +34,33 @@ type Storage interface { deviceAuthenticate } +// simple counter for request IDs +var counter atomic.Int64 + // SetupServer creates an OIDC server with Issuer=http://localhost: // // Use one of the pre-made clients in storage/clients.go or register a new one. -func SetupServer(issuer string, storage Storage) chi.Router { +func SetupServer(issuer string, storage Storage, logger *slog.Logger) chi.Router { // the OpenID Provider requires a 32-byte key for (token) encryption // be sure to create a proper crypto random key and manage it securely! key := sha256.Sum256([]byte("test")) router := chi.NewRouter() + router.Use(logging.Middleware( + logging.WithLogger(logger), + logging.WithIDFunc(func() slog.Attr { + return slog.Int64("id", counter.Add(1)) + }), + )) // for simplicity, we provide a very small default page for users who have signed out router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) { - _, err := w.Write([]byte("signed out successfully")) - if err != nil { - log.Printf("error serving logged out page: %v", err) - } + w.Write([]byte("signed out successfully")) + // no need to check/log error, this will be handeled by the middleware. }) // creation of the OpenIDProvider with the just created in-memory Storage - provider, err := newOP(storage, issuer, key) + provider, err := newOP(storage, issuer, key, logger) if err != nil { log.Fatal(err) } @@ -80,7 +90,7 @@ func SetupServer(issuer string, storage Storage) chi.Router { // newOP will create an OpenID Provider for localhost on a specified port with a given encryption key // and a predefined default logout uri // it will enable all options (see descriptions) -func newOP(storage op.Storage, issuer string, key [32]byte) (op.OpenIDProvider, error) { +func newOP(storage op.Storage, issuer string, key [32]byte, logger *slog.Logger) (op.OpenIDProvider, error) { config := &op.Config{ CryptoKey: key, @@ -117,6 +127,8 @@ func newOP(storage op.Storage, issuer string, key [32]byte) (op.OpenIDProvider, op.WithAllowInsecure(), // as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth op.WithCustomAuthEndpoint(op.NewEndpoint("auth")), + // Pass our logger to the OP + op.WithLogger(logger.WithGroup("op")), ) if err != nil { return nil, err diff --git a/example/server/main.go b/example/server/main.go index ee27bba..a1cc461 100644 --- a/example/server/main.go +++ b/example/server/main.go @@ -2,11 +2,12 @@ package main import ( "fmt" - "log" "net/http" + "os" "github.com/zitadel/oidc/v3/example/server/exampleop" "github.com/zitadel/oidc/v3/example/server/storage" + "golang.org/x/exp/slog" ) func main() { @@ -20,16 +21,22 @@ func main() { // in this example it will be handled in-memory storage := storage.NewStorage(storage.NewUserStore(issuer)) - router := exampleop.SetupServer(issuer, storage) + logger := slog.New( + slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelDebug, + }), + ) + router := exampleop.SetupServer(issuer, storage, logger) server := &http.Server{ Addr: ":" + port, Handler: router, } - log.Printf("server listening on http://localhost:%s/", port) - log.Println("press ctrl+c to stop") + logger.Info("server listening, press ctrl+c to stop", "addr", fmt.Sprintf("http://localhost:%s/", port)) err := server.ListenAndServe() - if err != nil { - log.Fatal(err) + if err != http.ErrServerClosed { + logger.Error("server terminated", "error", err) + os.Exit(1) } } diff --git a/example/server/storage/oidc.go b/example/server/storage/oidc.go index b56ad09..63afcf9 100644 --- a/example/server/storage/oidc.go +++ b/example/server/storage/oidc.go @@ -3,6 +3,7 @@ package storage import ( "time" + "golang.org/x/exp/slog" "golang.org/x/text/language" "github.com/zitadel/oidc/v3/pkg/oidc" @@ -41,6 +42,19 @@ type AuthRequest struct { authTime time.Time } +// LogValue allows you to define which fields will be logged. +// Implements the [slog.LogValuer] +func (a *AuthRequest) LogValue() slog.Value { + return slog.GroupValue( + slog.String("id", a.ID), + slog.Time("creation_date", a.CreationDate), + slog.Any("scopes", a.Scopes), + slog.String("response_type", string(a.ResponseType)), + slog.String("app_id", a.ApplicationID), + slog.String("callback_uri", a.CallbackURI), + ) +} + func (a *AuthRequest) GetID() string { return a.ID } diff --git a/go.mod b/go.mod index 610d2a1..62aa39b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/zitadel/oidc/v3 -go 1.18 +go 1.19 require ( github.com/go-chi/chi v1.5.4 @@ -11,9 +11,11 @@ require ( github.com/jeremija/gosubmit v0.2.7 github.com/muhlemmer/gu v0.3.1 github.com/rs/cors v1.9.0 - github.com/sirupsen/logrus v1.9.0 + github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.2 + github.com/zitadel/logging v0.4.0 github.com/zitadel/schema v1.3.0 + golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 golang.org/x/oauth2 v0.7.0 golang.org/x/text v0.9.0 gopkg.in/square/go-jose.v2 v2.6.0 @@ -27,7 +29,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/crypto v0.7.0 // indirect golang.org/x/net v0.9.0 // indirect - golang.org/x/sys v0.7.0 // indirect + golang.org/x/sys v0.11.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.29.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index c9c8562..9c44f0f 100644 --- a/go.sum +++ b/go.sum @@ -36,8 +36,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -47,12 +47,16 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zitadel/logging v0.4.0 h1:lRAIFgaRoJpLNbsL7jtIYHcMDoEJP9QZB4GqMfl4xaA= +github.com/zitadel/logging v0.4.0/go.mod h1:6uALRJawpkkuUPCkgzfgcPR3c2N908wqnOnIrRelUFc= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= +golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -73,8 +77,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -100,6 +104,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/client/client.go b/pkg/client/client.go index e3efd61..7b76dfd 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -14,6 +14,7 @@ import ( "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" + "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/crypto" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" @@ -37,6 +38,10 @@ func Discover(ctx context.Context, issuer string, httpClient *http.Client, wellK if err != nil { return nil, err } + if logger, ok := logging.FromContext(ctx); ok { + logger.Debug("discover", "config", discoveryConfig) + } + if discoveryConfig.Issuer != issuer { return nil, oidc.ErrIssuerInvalid } diff --git a/pkg/client/integration_test.go b/pkg/client/integration_test.go index 073efef..7cbb62e 100644 --- a/pkg/client/integration_test.go +++ b/pkg/client/integration_test.go @@ -19,6 +19,7 @@ import ( "github.com/jeremija/gosubmit" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/exp/slog" "github.com/zitadel/oidc/v3/example/server/exampleop" "github.com/zitadel/oidc/v3/example/server/storage" @@ -29,6 +30,13 @@ import ( "github.com/zitadel/oidc/v3/pkg/oidc" ) +var Logger = slog.New( + slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelDebug, + }), +) + var CTX context.Context func TestMain(m *testing.M) { @@ -49,7 +57,7 @@ func TestRelyingPartySession(t *testing.T) { opServer := httptest.NewServer(&dh) defer opServer.Close() t.Logf("auth server at %s", opServer.URL) - dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage) + dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger) seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano())) clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25) @@ -100,7 +108,7 @@ func TestResourceServerTokenExchange(t *testing.T) { opServer := httptest.NewServer(&dh) defer opServer.Close() t.Logf("auth server at %s", opServer.URL) - dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage) + dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger) seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano())) clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25) diff --git a/pkg/client/rp/device.go b/pkg/client/rp/device.go index 390c8cf..02c647e 100644 --- a/pkg/client/rp/device.go +++ b/pkg/client/rp/device.go @@ -33,6 +33,7 @@ func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc. // in RFC 8628, section 3.1 and 3.2: // https://www.rfc-editor.org/rfc/rfc8628#section-3.1 func DeviceAuthorization(ctx context.Context, scopes []string, rp RelyingParty, authFn any) (*oidc.DeviceAuthorizationResponse, error) { + ctx = logCtxWithRPData(ctx, rp, "function", "DeviceAuthorization") req, err := newDeviceClientCredentialsRequest(scopes, rp) if err != nil { return nil, err @@ -45,6 +46,7 @@ func DeviceAuthorization(ctx context.Context, scopes []string, rp RelyingParty, // by means of polling as defined in RFC, section 3.3 and 3.4: // https://www.rfc-editor.org/rfc/rfc8628#section-3.4 func DeviceAccessToken(ctx context.Context, deviceCode string, interval time.Duration, rp RelyingParty) (resp *oidc.AccessTokenResponse, err error) { + ctx = logCtxWithRPData(ctx, rp, "function", "DeviceAccessToken") req := &client.DeviceAccessTokenRequest{ DeviceAccessTokenRequest: oidc.DeviceAccessTokenRequest{ GrantType: oidc.GrantTypeDeviceCode, diff --git a/pkg/client/rp/log.go b/pkg/client/rp/log.go new file mode 100644 index 0000000..6056fa2 --- /dev/null +++ b/pkg/client/rp/log.go @@ -0,0 +1,17 @@ +package rp + +import ( + "context" + + "github.com/zitadel/logging" + "golang.org/x/exp/slog" +) + +func logCtxWithRPData(ctx context.Context, rp RelyingParty, attrs ...any) context.Context { + logger, ok := rp.Logger(ctx) + if !ok { + return ctx + } + logger = logger.With(slog.Group("rp", attrs...)) + return logging.ToContext(ctx, logger) +} diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 5597c9d..34cdb39 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -10,6 +10,8 @@ import ( "time" "github.com/google/uuid" + "github.com/zitadel/logging" + "golang.org/x/exp/slog" "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" @@ -67,6 +69,9 @@ type RelyingParty interface { // ErrorHandler returns the handler used for callback errors ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) + + // Logger from the context, or a fallback if set. + Logger(context.Context) (logger *slog.Logger, ok bool) } type ErrorHandler func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) @@ -90,6 +95,7 @@ type relyingParty struct { idTokenVerifier *IDTokenVerifier verifierOpts []VerifierOption signer jose.Signer + logger *slog.Logger } func (rp *relyingParty) OAuthConfig() *oauth2.Config { @@ -150,6 +156,14 @@ func (rp *relyingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, return rp.errorHandler } +func (rp *relyingParty) Logger(ctx context.Context) (logger *slog.Logger, ok bool) { + logger, ok = logging.FromContext(ctx) + if ok { + return logger, ok + } + return rp.logger, rp.logger != nil +} + // NewRelyingPartyOAuth creates an (OAuth2) RelyingParty with the given // OAuth2 Config and possible configOptions // it will use the AuthURL and TokenURL set in config @@ -194,6 +208,7 @@ func NewRelyingPartyOIDC(ctx context.Context, issuer, clientID, clientSecret, re return nil, err } } + ctx = logCtxWithRPData(ctx, rp, "function", "NewRelyingPartyOIDC") discoveryConfiguration, err := client.Discover(ctx, rp.issuer, rp.httpClient, rp.DiscoveryEndpoint) if err != nil { return nil, err @@ -281,6 +296,15 @@ func WithJWTProfile(signerFromKey SignerFromKey) Option { } } +// WithLogger sets a logger that is used +// in case the request context does not contain a logger. +func WithLogger(logger *slog.Logger) Option { + return func(rp *relyingParty) error { + rp.logger = logger + return nil + } +} + type SignerFromKey func() (jose.Signer, error) func SignerFromKeyPath(path string) SignerFromKey { @@ -378,6 +402,7 @@ func verifyTokenResponse[C oidc.IDClaims](ctx context.Context, token *oauth2.Tok // CodeExchange handles the oauth2 code exchange, extracting and validating the id_token // returning it parsed together with the oauth2 tokens (access, refresh) func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens[C], err error) { + ctx = logCtxWithRPData(ctx, rp, "function", "CodeExchange") ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient()) codeOpts := make([]oauth2.AuthCodeOption, 0) for _, opt := range opts { @@ -467,6 +492,7 @@ func UserinfoCallback[C oidc.IDClaims, U SubjectGetter](f CodeExchangeUserinfoCa // [UserInfo]: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo func Userinfo[U SubjectGetter](ctx context.Context, token, tokenType, subject string, rp RelyingParty) (userinfo U, err error) { var nilU U + ctx = logCtxWithRPData(ctx, rp, "function", "Userinfo") req, err := http.NewRequestWithContext(ctx, http.MethodGet, rp.UserinfoEndpoint(), nil) if err != nil { @@ -546,7 +572,7 @@ func withURLParam(key, value string) func() []oauth2.AuthCodeOption { // This is the generalized, unexported, function used by both // URLParamOpt and AuthURLOpt. func withPrompt(prompt ...string) func() []oauth2.AuthCodeOption { - return withURLParam("prompt", oidc.SpaceDelimitedArray(prompt).Encode()) + return withURLParam("prompt", oidc.SpaceDelimitedArray(prompt).String()) } type URLParamOpt func() []oauth2.AuthCodeOption @@ -621,6 +647,7 @@ type RefreshTokenRequest struct { // the IDToken and AccessToken will be verfied // and the IDToken and IDTokenClaims fields will be populated in the returned object. func RefreshTokens[C oidc.IDClaims](ctx context.Context, rp RelyingParty, refreshToken, clientAssertion, clientAssertionType string) (*oidc.Tokens[C], error) { + ctx = logCtxWithRPData(ctx, rp, "function", "RefreshTokens") request := RefreshTokenRequest{ RefreshToken: refreshToken, Scopes: rp.OAuthConfig().Scopes, @@ -644,6 +671,7 @@ func RefreshTokens[C oidc.IDClaims](ctx context.Context, rp RelyingParty, refres } func EndSession(ctx context.Context, rp RelyingParty, idToken, optionalRedirectURI, optionalState string) (*url.URL, error) { + ctx = logCtxWithRPData(ctx, rp, "function", "EndSession") request := oidc.EndSessionRequest{ IdTokenHint: idToken, ClientID: rp.OAuthConfig().ClientID, @@ -659,6 +687,7 @@ func EndSession(ctx context.Context, rp RelyingParty, idToken, optionalRedirectU // // tokenTypeHint should be either "id_token" or "refresh_token". func RevokeToken(ctx context.Context, rp RelyingParty, token string, tokenTypeHint string) error { + ctx = logCtxWithRPData(ctx, rp, "function", "RevokeToken") request := client.RevokeRequest{ Token: token, TokenTypeHint: tokenTypeHint, diff --git a/pkg/oidc/authorization.go b/pkg/oidc/authorization.go index d8bf336..7e7c30c 100644 --- a/pkg/oidc/authorization.go +++ b/pkg/oidc/authorization.go @@ -1,5 +1,9 @@ package oidc +import ( + "golang.org/x/exp/slog" +) + const ( // ScopeOpenID defines the scope `openid` // OpenID Connect requests MUST contain the `openid` scope value @@ -86,6 +90,15 @@ type AuthRequest struct { RequestParam string `schema:"request"` } +func (a *AuthRequest) LogValue() slog.Value { + return slog.GroupValue( + slog.Any("scopes", a.Scopes), + slog.String("response_type", string(a.ResponseType)), + slog.String("client_id", a.ClientID), + slog.String("redirect_uri", a.RedirectURI), + ) +} + // GetRedirectURI returns the redirect_uri value for the ErrAuthRequest interface func (a *AuthRequest) GetRedirectURI() string { return a.RedirectURI diff --git a/pkg/oidc/authorization_test.go b/pkg/oidc/authorization_test.go new file mode 100644 index 0000000..573d65c --- /dev/null +++ b/pkg/oidc/authorization_test.go @@ -0,0 +1,27 @@ +//go:build go1.20 + +package oidc + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/exp/slog" +) + +func TestAuthRequest_LogValue(t *testing.T) { + a := &AuthRequest{ + Scopes: SpaceDelimitedArray{"a", "b"}, + ResponseType: "respType", + ClientID: "123", + RedirectURI: "http://example.com/callback", + } + want := slog.GroupValue( + slog.Any("scopes", SpaceDelimitedArray{"a", "b"}), + slog.String("response_type", "respType"), + slog.String("client_id", "123"), + slog.String("redirect_uri", "http://example.com/callback"), + ) + got := a.LogValue() + assert.Equal(t, want, got) +} diff --git a/pkg/oidc/error.go b/pkg/oidc/error.go index 79acecd..07a9069 100644 --- a/pkg/oidc/error.go +++ b/pkg/oidc/error.go @@ -3,6 +3,8 @@ package oidc import ( "errors" "fmt" + + "golang.org/x/exp/slog" ) type errorType string @@ -171,3 +173,34 @@ func DefaultToServerError(err error, description string) *Error { } return oauth } + +func (e *Error) LogLevel() slog.Level { + level := slog.LevelWarn + if e.ErrorType == ServerError { + level = slog.LevelError + } + if e.ErrorType == AuthorizationPending { + level = slog.LevelInfo + } + return level +} + +func (e *Error) LogValue() slog.Value { + attrs := make([]slog.Attr, 0, 5) + if e.Parent != nil { + attrs = append(attrs, slog.Any("parent", e.Parent)) + } + if e.Description != "" { + attrs = append(attrs, slog.String("description", e.Description)) + } + if e.ErrorType != "" { + attrs = append(attrs, slog.String("type", string(e.ErrorType))) + } + if e.State != "" { + attrs = append(attrs, slog.String("state", e.State)) + } + if e.redirectDisabled { + attrs = append(attrs, slog.Bool("redirect_disabled", e.redirectDisabled)) + } + return slog.GroupValue(attrs...) +} diff --git a/pkg/oidc/error_go120_test.go b/pkg/oidc/error_go120_test.go new file mode 100644 index 0000000..399d7f7 --- /dev/null +++ b/pkg/oidc/error_go120_test.go @@ -0,0 +1,83 @@ +//go:build go1.20 + +package oidc + +import ( + "io" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/exp/slog" +) + +func TestError_LogValue(t *testing.T) { + type fields struct { + Parent error + ErrorType errorType + Description string + State string + redirectDisabled bool + } + tests := []struct { + name string + fields fields + want slog.Value + }{ + { + name: "parent", + fields: fields{ + Parent: io.EOF, + }, + want: slog.GroupValue(slog.Any("parent", io.EOF)), + }, + { + name: "description", + fields: fields{ + Description: "oops", + }, + want: slog.GroupValue(slog.String("description", "oops")), + }, + { + name: "errorType", + fields: fields{ + ErrorType: ExpiredToken, + }, + want: slog.GroupValue(slog.String("type", string(ExpiredToken))), + }, + { + name: "state", + fields: fields{ + State: "123", + }, + want: slog.GroupValue(slog.String("state", "123")), + }, + { + name: "all fields", + fields: fields{ + Parent: io.EOF, + Description: "oops", + ErrorType: ExpiredToken, + State: "123", + }, + want: slog.GroupValue( + slog.Any("parent", io.EOF), + slog.String("description", "oops"), + slog.String("type", string(ExpiredToken)), + slog.String("state", "123"), + ), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Error{ + Parent: tt.fields.Parent, + ErrorType: tt.fields.ErrorType, + Description: tt.fields.Description, + State: tt.fields.State, + redirectDisabled: tt.fields.redirectDisabled, + } + got := e.LogValue() + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/oidc/error_test.go b/pkg/oidc/error_test.go new file mode 100644 index 0000000..0554c8f --- /dev/null +++ b/pkg/oidc/error_test.go @@ -0,0 +1,81 @@ +package oidc + +import ( + "io" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/exp/slog" +) + +func TestDefaultToServerError(t *testing.T) { + type args struct { + err error + description string + } + tests := []struct { + name string + args args + want *Error + }{ + { + name: "default", + args: args{ + err: io.ErrClosedPipe, + description: "oops", + }, + want: &Error{ + ErrorType: ServerError, + Description: "oops", + Parent: io.ErrClosedPipe, + }, + }, + { + name: "our Error", + args: args{ + err: ErrAccessDenied(), + description: "oops", + }, + want: &Error{ + ErrorType: AccessDenied, + Description: "The authorization request was denied.", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := DefaultToServerError(tt.args.err, tt.args.description) + assert.ErrorIs(t, got, tt.want) + }) + } +} + +func TestError_LogLevel(t *testing.T) { + tests := []struct { + name string + err *Error + want slog.Level + }{ + { + name: "server error", + err: ErrServerError(), + want: slog.LevelError, + }, + { + name: "authorization pending", + err: ErrAuthorizationPending(), + want: slog.LevelInfo, + }, + { + name: "some other error", + err: ErrAccessDenied(), + want: slog.LevelWarn, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.err.LogLevel() + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index 86ee1e0..5db8bad 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -106,7 +106,7 @@ type ResponseType string type ResponseMode string -func (s SpaceDelimitedArray) Encode() string { +func (s SpaceDelimitedArray) String() string { return strings.Join(s, " ") } @@ -116,11 +116,11 @@ func (s *SpaceDelimitedArray) UnmarshalText(text []byte) error { } func (s SpaceDelimitedArray) MarshalText() ([]byte, error) { - return []byte(s.Encode()), nil + return []byte(s.String()), nil } func (s SpaceDelimitedArray) MarshalJSON() ([]byte, error) { - return json.Marshal((s).Encode()) + return json.Marshal((s).String()) } func (s *SpaceDelimitedArray) UnmarshalJSON(data []byte) error { @@ -165,7 +165,7 @@ func (s SpaceDelimitedArray) Value() (driver.Value, error) { func NewEncoder() *schema.Encoder { e := schema.NewEncoder() e.RegisterEncoder(SpaceDelimitedArray{}, func(value reflect.Value) string { - return value.Interface().(SpaceDelimitedArray).Encode() + return value.Interface().(SpaceDelimitedArray).String() }) return e } diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 7af3779..7610248 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -14,6 +14,7 @@ import ( httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" str "github.com/zitadel/oidc/v3/pkg/strings" + "golang.org/x/exp/slog" ) type AuthRequest interface { @@ -41,6 +42,7 @@ type Authorizer interface { IDTokenHintVerifier(context.Context) *IDTokenHintVerifier Crypto() Crypto RequestObjectSupported() bool + Logger() *slog.Logger } // AuthorizeValidator is an extension of Authorizer interface @@ -67,23 +69,23 @@ func authorizeCallbackHandler(authorizer Authorizer) func(http.ResponseWriter, * func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { authReq, err := ParseAuthorizeRequest(r, authorizer.Decoder()) if err != nil { - AuthRequestError(w, r, nil, err, authorizer.Encoder()) + AuthRequestError(w, r, nil, err, authorizer) return } ctx := r.Context() if authReq.RequestParam != "" && authorizer.RequestObjectSupported() { authReq, err = ParseRequestObject(ctx, authReq, authorizer.Storage(), IssuerFromContext(ctx)) if err != nil { - AuthRequestError(w, r, authReq, err, authorizer.Encoder()) + AuthRequestError(w, r, authReq, err, authorizer) return } } if authReq.ClientID == "" { - AuthRequestError(w, r, authReq, fmt.Errorf("auth request is missing client_id"), authorizer.Encoder()) + AuthRequestError(w, r, authReq, fmt.Errorf("auth request is missing client_id"), authorizer) return } if authReq.RedirectURI == "" { - AuthRequestError(w, r, authReq, fmt.Errorf("auth request is missing redirect_uri"), authorizer.Encoder()) + AuthRequestError(w, r, authReq, fmt.Errorf("auth request is missing redirect_uri"), authorizer) return } validation := ValidateAuthRequest @@ -92,21 +94,21 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { } userID, err := validation(ctx, authReq, authorizer.Storage(), authorizer.IDTokenHintVerifier(ctx)) if err != nil { - AuthRequestError(w, r, authReq, err, authorizer.Encoder()) + AuthRequestError(w, r, authReq, err, authorizer) return } if authReq.RequestParam != "" { - AuthRequestError(w, r, authReq, oidc.ErrRequestNotSupported(), authorizer.Encoder()) + AuthRequestError(w, r, authReq, oidc.ErrRequestNotSupported(), authorizer) return } req, err := authorizer.Storage().CreateAuthRequest(ctx, authReq, userID) if err != nil { - AuthRequestError(w, r, authReq, oidc.DefaultToServerError(err, "unable to save auth request"), authorizer.Encoder()) + AuthRequestError(w, r, authReq, oidc.DefaultToServerError(err, "unable to save auth request"), authorizer) return } client, err := authorizer.Storage().GetClientByClientID(ctx, req.GetClientID()) if err != nil { - AuthRequestError(w, r, req, oidc.DefaultToServerError(err, "unable to retrieve client by id"), authorizer.Encoder()) + AuthRequestError(w, r, req, oidc.DefaultToServerError(err, "unable to retrieve client by id"), authorizer) return } RedirectToLogin(req.GetID(), client, w, r) @@ -406,18 +408,18 @@ func RedirectToLogin(authReqID string, client Client, w http.ResponseWriter, r * func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { id, err := ParseAuthorizeCallbackRequest(r) if err != nil { - AuthRequestError(w, r, nil, err, authorizer.Encoder()) + AuthRequestError(w, r, nil, err, authorizer) return } authReq, err := authorizer.Storage().AuthRequestByID(r.Context(), id) if err != nil { - AuthRequestError(w, r, nil, err, authorizer.Encoder()) + AuthRequestError(w, r, nil, err, authorizer) return } if !authReq.Done() { AuthRequestError(w, r, authReq, oidc.ErrInteractionRequired().WithDescription("Unfortunately, the user may be not logged in and/or additional interaction is required."), - authorizer.Encoder()) + authorizer) return } AuthResponse(authReq, authorizer, w, r) @@ -438,7 +440,7 @@ func ParseAuthorizeCallbackRequest(r *http.Request) (id string, err error) { func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWriter, r *http.Request) { client, err := authorizer.Storage().GetClientByClientID(r.Context(), authReq.GetClientID()) if err != nil { - AuthRequestError(w, r, authReq, err, authorizer.Encoder()) + AuthRequestError(w, r, authReq, err, authorizer) return } if authReq.GetResponseType() == oidc.ResponseTypeCode { @@ -452,7 +454,7 @@ func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWri func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer) { code, err := CreateAuthRequestCode(r.Context(), authReq, authorizer.Storage(), authorizer.Crypto()) if err != nil { - AuthRequestError(w, r, authReq, err, authorizer.Encoder()) + AuthRequestError(w, r, authReq, err, authorizer) return } codeResponse := struct { @@ -464,7 +466,7 @@ func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthReques } callback, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), &codeResponse, authorizer.Encoder()) if err != nil { - AuthRequestError(w, r, authReq, err, authorizer.Encoder()) + AuthRequestError(w, r, authReq, err, authorizer) return } http.Redirect(w, r, callback, http.StatusFound) @@ -475,12 +477,12 @@ func AuthResponseToken(w http.ResponseWriter, r *http.Request, authReq AuthReque createAccessToken := authReq.GetResponseType() != oidc.ResponseTypeIDTokenOnly resp, err := CreateTokenResponse(r.Context(), authReq, client, authorizer, createAccessToken, "", "") if err != nil { - AuthRequestError(w, r, authReq, err, authorizer.Encoder()) + AuthRequestError(w, r, authReq, err, authorizer) return } callback, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), resp, authorizer.Encoder()) if err != nil { - AuthRequestError(w, r, authReq, err, authorizer.Encoder()) + AuthRequestError(w, r, authReq, err, authorizer) return } http.Redirect(w, r, callback, http.StatusFound) diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index df340b6..42fd0aa 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -18,6 +18,7 @@ import ( "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/oidc/v3/pkg/op/mock" "github.com/zitadel/schema" + "golang.org/x/exp/slog" ) func TestAuthorize(t *testing.T) { @@ -38,7 +39,7 @@ func TestAuthorize(t *testing.T) { expect := authorizer.EXPECT() expect.Decoder().Return(schema.NewDecoder()) - expect.Encoder().Return(schema.NewEncoder()) + expect.Logger().Return(slog.Default()) if tt.expect != nil { tt.expect(expect) diff --git a/pkg/op/device.go b/pkg/op/device.go index 09c7fca..029bed8 100644 --- a/pkg/op/device.go +++ b/pkg/op/device.go @@ -57,7 +57,7 @@ var ( func DeviceAuthorizationHandler(o OpenIDProvider) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { if err := DeviceAuthorization(w, r, o); err != nil { - RequestError(w, r, err) + RequestError(w, r, err, o.Logger()) } } } @@ -190,7 +190,7 @@ func (r *deviceAccessTokenRequest) GetScopes() []string { func DeviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { if err := deviceAccessToken(w, r, exchanger); err != nil { - RequestError(w, r, err) + RequestError(w, r, err, exchanger.Logger()) } } diff --git a/pkg/op/error.go b/pkg/op/error.go index b2d84ae..9981fec 100644 --- a/pkg/op/error.go +++ b/pkg/op/error.go @@ -5,6 +5,7 @@ import ( httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" + "golang.org/x/exp/slog" ) type ErrAuthRequest interface { @@ -13,13 +14,31 @@ type ErrAuthRequest interface { GetState() string } -func AuthRequestError(w http.ResponseWriter, r *http.Request, authReq ErrAuthRequest, err error, encoder httphelper.Encoder) { +// LogAuthRequest is an optional interface, +// that allows logging AuthRequest fields. +// If the AuthRequest does not implement this interface, +// no details shall be printed to the logs. +type LogAuthRequest interface { + ErrAuthRequest + slog.LogValuer +} + +func AuthRequestError(w http.ResponseWriter, r *http.Request, authReq ErrAuthRequest, err error, authorizer Authorizer) { + e := oidc.DefaultToServerError(err, err.Error()) + logger := authorizer.Logger().With("oidc_error", e) + if authReq == nil { + logger.Log(r.Context(), e.LogLevel(), "auth request") http.Error(w, err.Error(), http.StatusBadRequest) return } - e := oidc.DefaultToServerError(err, err.Error()) + + if logAuthReq, ok := authReq.(LogAuthRequest); ok { + logger = logger.With("auth_request", logAuthReq) + } + if authReq.GetRedirectURI() == "" || e.IsRedirectDisabled() { + logger.Log(r.Context(), e.LogLevel(), "auth request: not redirecting") http.Error(w, e.Description, http.StatusBadRequest) return } @@ -28,19 +47,22 @@ func AuthRequestError(w http.ResponseWriter, r *http.Request, authReq ErrAuthReq if rm, ok := authReq.(interface{ GetResponseMode() oidc.ResponseMode }); ok { responseMode = rm.GetResponseMode() } - url, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), responseMode, e, encoder) + url, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), responseMode, e, authorizer.Encoder()) if err != nil { + logger.ErrorContext(r.Context(), "auth response URL", "error", err) http.Error(w, err.Error(), http.StatusBadRequest) return } + logger.Log(r.Context(), e.LogLevel(), "auth request") http.Redirect(w, r, url, http.StatusFound) } -func RequestError(w http.ResponseWriter, r *http.Request, err error) { +func RequestError(w http.ResponseWriter, r *http.Request, err error, logger *slog.Logger) { e := oidc.DefaultToServerError(err, err.Error()) status := http.StatusBadRequest if e.ErrorType == oidc.InvalidClient { - status = 401 + status = http.StatusUnauthorized } + logger.Log(r.Context(), e.LogLevel(), "request error", "oidc_error", e) httphelper.MarshalJSONWithStatus(w, e, status) } diff --git a/pkg/op/error_test.go b/pkg/op/error_test.go new file mode 100644 index 0000000..dc5ef11 --- /dev/null +++ b/pkg/op/error_test.go @@ -0,0 +1,277 @@ +package op + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/schema" + "golang.org/x/exp/slog" +) + +func TestAuthRequestError(t *testing.T) { + type args struct { + authReq ErrAuthRequest + err error + } + tests := []struct { + name string + args args + wantCode int + wantHeaders map[string]string + wantBody string + wantLog string + }{ + { + name: "nil auth request", + args: args{ + authReq: nil, + err: io.ErrClosedPipe, + }, + wantCode: http.StatusBadRequest, + wantBody: "io: read/write on closed pipe\n", + wantLog: `{ + "level":"ERROR", + "msg":"auth request", + "time":"not", + "oidc_error":{ + "description":"io: read/write on closed pipe", + "parent":"io: read/write on closed pipe", + "type":"server_error" + } + }`, + }, + { + name: "auth request, no redirect URI", + args: args{ + authReq: &oidc.AuthRequest{ + Scopes: oidc.SpaceDelimitedArray{"a", "b"}, + ResponseType: "responseType", + ClientID: "123", + State: "state1", + ResponseMode: oidc.ResponseModeQuery, + }, + err: oidc.ErrInteractionRequired().WithDescription("sign in"), + }, + wantCode: http.StatusBadRequest, + wantBody: "sign in\n", + wantLog: `{ + "level":"WARN", + "msg":"auth request: not redirecting", + "time":"not", + "auth_request":{ + "client_id":"123", + "redirect_uri":"", + "response_type":"responseType", + "scopes":"a b" + }, + "oidc_error":{ + "description":"sign in", + "type":"interaction_required" + } + }`, + }, + { + name: "auth request, redirect disabled", + args: args{ + authReq: &oidc.AuthRequest{ + Scopes: oidc.SpaceDelimitedArray{"a", "b"}, + ResponseType: "responseType", + ClientID: "123", + RedirectURI: "http://example.com/callback", + State: "state1", + ResponseMode: oidc.ResponseModeQuery, + }, + err: oidc.ErrInvalidRequestRedirectURI().WithDescription("oops"), + }, + wantCode: http.StatusBadRequest, + wantBody: "oops\n", + wantLog: `{ + "level":"WARN", + "msg":"auth request: not redirecting", + "time":"not", + "auth_request":{ + "client_id":"123", + "redirect_uri":"http://example.com/callback", + "response_type":"responseType", + "scopes":"a b" + }, + "oidc_error":{ + "description":"oops", + "type":"invalid_request", + "redirect_disabled":true + } + }`, + }, + { + name: "auth request, url parse error", + args: args{ + authReq: &oidc.AuthRequest{ + Scopes: oidc.SpaceDelimitedArray{"a", "b"}, + ResponseType: "responseType", + ClientID: "123", + RedirectURI: "can't parse this!\n", + State: "state1", + ResponseMode: oidc.ResponseModeQuery, + }, + err: oidc.ErrInteractionRequired().WithDescription("sign in"), + }, + wantCode: http.StatusBadRequest, + wantBody: "ErrorType=server_error Parent=parse \"can't parse this!\\n\": net/url: invalid control character in URL\n", + wantLog: `{ + "level":"ERROR", + "msg":"auth response URL", + "time":"not", + "auth_request":{ + "client_id":"123", + "redirect_uri":"can't parse this!\n", + "response_type":"responseType", + "scopes":"a b" + }, + "error":{ + "type":"server_error", + "parent":"parse \"can't parse this!\\n\": net/url: invalid control character in URL" + }, + "oidc_error":{ + "description":"sign in", + "type":"interaction_required" + } + }`, + }, + { + name: "auth request redirect", + args: args{ + authReq: &oidc.AuthRequest{ + Scopes: oidc.SpaceDelimitedArray{"a", "b"}, + ResponseType: "responseType", + ClientID: "123", + RedirectURI: "http://example.com/callback", + State: "state1", + ResponseMode: oidc.ResponseModeQuery, + }, + err: oidc.ErrInteractionRequired().WithDescription("sign in"), + }, + wantCode: http.StatusFound, + wantHeaders: map[string]string{"Location": "http://example.com/callback?error=interaction_required&error_description=sign+in&state=state1"}, + wantLog: `{ + "level":"WARN", + "msg":"auth request", + "time":"not", + "auth_request":{ + "client_id":"123", + "redirect_uri":"http://example.com/callback", + "response_type":"responseType", + "scopes":"a b" + }, + "oidc_error":{ + "description":"sign in", + "type":"interaction_required" + } + }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logOut := new(strings.Builder) + authorizer := &Provider{ + encoder: schema.NewEncoder(), + logger: slog.New( + slog.NewJSONHandler(logOut, &slog.HandlerOptions{ + Level: slog.LevelInfo, + }).WithAttrs([]slog.Attr{slog.String("time", "not")}), + ), + } + + w := httptest.NewRecorder() + r := httptest.NewRequest("POST", "/path", nil) + AuthRequestError(w, r, tt.args.authReq, tt.args.err, authorizer) + + res := w.Result() + defer res.Body.Close() + + assert.Equal(t, tt.wantCode, res.StatusCode) + for key, wantHeader := range tt.wantHeaders { + gotHeader := res.Header.Get(key) + assert.Equalf(t, wantHeader, gotHeader, "header %q", key) + } + gotBody, err := io.ReadAll(res.Body) + require.NoError(t, err, "read result body") + assert.Equal(t, tt.wantBody, string(gotBody), "result body") + + gotLog := logOut.String() + t.Log(gotLog) + assert.JSONEq(t, tt.wantLog, gotLog, "log output") + }) + } +} + +func TestRequestError(t *testing.T) { + tests := []struct { + name string + err error + wantCode int + wantBody string + wantLog string + }{ + { + name: "server error", + err: io.ErrClosedPipe, + wantCode: http.StatusBadRequest, + wantBody: `{"error":"server_error", "error_description":"io: read/write on closed pipe"}`, + wantLog: `{ + "level":"ERROR", + "msg":"request error", + "time":"not", + "oidc_error":{ + "parent":"io: read/write on closed pipe", + "description":"io: read/write on closed pipe", + "type":"server_error"} + }`, + }, + { + name: "invalid client", + err: oidc.ErrInvalidClient().WithDescription("not good"), + wantCode: http.StatusUnauthorized, + wantBody: `{"error":"invalid_client", "error_description":"not good"}`, + wantLog: `{ + "level":"WARN", + "msg":"request error", + "time":"not", + "oidc_error":{ + "description":"not good", + "type":"invalid_client"} + }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logOut := new(strings.Builder) + logger := slog.New( + slog.NewJSONHandler(logOut, &slog.HandlerOptions{ + Level: slog.LevelInfo, + }).WithAttrs([]slog.Attr{slog.String("time", "not")}), + ) + w := httptest.NewRecorder() + r := httptest.NewRequest("POST", "/path", nil) + RequestError(w, r, tt.err, logger) + + res := w.Result() + defer res.Body.Close() + + assert.Equal(t, tt.wantCode, res.StatusCode, "status code") + + gotBody, err := io.ReadAll(res.Body) + require.NoError(t, err, "read result body") + assert.JSONEq(t, tt.wantBody, string(gotBody), "result body") + + gotLog := logOut.String() + t.Log(gotLog) + assert.JSONEq(t, tt.wantLog, gotLog, "log output") + }) + } +} diff --git a/pkg/op/mock/authorizer.mock.go b/pkg/op/mock/authorizer.mock.go index a0c67e3..e4297cb 100644 --- a/pkg/op/mock/authorizer.mock.go +++ b/pkg/op/mock/authorizer.mock.go @@ -11,6 +11,7 @@ import ( gomock "github.com/golang/mock/gomock" http "github.com/zitadel/oidc/v3/pkg/http" op "github.com/zitadel/oidc/v3/pkg/op" + slog "golang.org/x/exp/slog" ) // MockAuthorizer is a mock of Authorizer interface. @@ -92,6 +93,20 @@ func (mr *MockAuthorizerMockRecorder) IDTokenHintVerifier(arg0 interface{}) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenHintVerifier", reflect.TypeOf((*MockAuthorizer)(nil).IDTokenHintVerifier), arg0) } +// Logger mocks base method. +func (m *MockAuthorizer) Logger() *slog.Logger { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Logger") + ret0, _ := ret[0].(*slog.Logger) + return ret0 +} + +// Logger indicates an expected call of Logger. +func (mr *MockAuthorizerMockRecorder) Logger() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logger", reflect.TypeOf((*MockAuthorizer)(nil).Logger)) +} + // RequestObjectSupported mocks base method. func (m *MockAuthorizer) RequestObjectSupported() bool { m.ctrl.T.Helper() diff --git a/pkg/op/op.go b/pkg/op/op.go index 1fbe780..d8ae570 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -9,6 +9,7 @@ import ( "github.com/go-chi/chi" "github.com/rs/cors" "github.com/zitadel/schema" + "golang.org/x/exp/slog" "golang.org/x/text/language" "gopkg.in/square/go-jose.v2" @@ -79,6 +80,9 @@ type OpenIDProvider interface { DefaultLogoutRedirectURI() string Probes() []ProbesFn + // EXPERIMENTAL: Will change to log/slog import after we drop support for Go 1.20 + Logger() *slog.Logger + // Deprecated: Provider now implements http.Handler directly. HttpHandler() http.Handler } @@ -174,6 +178,7 @@ func newProvider(config *Config, storage Storage, issuer func(bool) (IssuerFromR storage: storage, endpoints: DefaultEndpoints, timer: make(<-chan time.Time), + logger: slog.Default(), } for _, optFunc := range opOpts { @@ -217,6 +222,7 @@ type Provider struct { timer <-chan time.Time accessTokenVerifierOpts []AccessTokenVerifierOpt idTokenHintVerifierOpts []IDTokenHintVerifierOpt + logger *slog.Logger } func (o *Provider) IssuerFromRequest(r *http.Request) string { @@ -375,6 +381,10 @@ func (o *Provider) Probes() []ProbesFn { } } +func (o *Provider) Logger() *slog.Logger { + return o.logger +} + // Deprecated: Provider now implements http.Handler directly. func (o *Provider) HttpHandler() http.Handler { return o @@ -523,6 +533,16 @@ func WithIDTokenHintVerifierOpts(opts ...IDTokenHintVerifierOpt) Option { } } +// WithLogger lets a logger other than slog.Default(). +// +// EXPERIMENTAL: Will change to log/slog import after we drop support for Go 1.20 +func WithLogger(logger *slog.Logger) Option { + return func(o *Provider) error { + o.logger = logger + return nil + } +} + func intercept(i IssuerFromRequest, interceptors ...HttpInterceptor) func(handler http.Handler) http.Handler { issuerInterceptor := NewIssuerInterceptor(i) return func(handler http.Handler) http.Handler { diff --git a/pkg/op/op_test.go b/pkg/op/op_test.go index d347d04..d33b39d 100644 --- a/pkg/op/op_test.go +++ b/pkg/op/op_test.go @@ -156,7 +156,7 @@ func TestRoutes(t *testing.T) { values: map[string]string{ "client_id": client.GetID(), "redirect_uri": "https://example.com", - "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(), + "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.String(), "response_type": string(oidc.ResponseTypeCode), }, wantCode: http.StatusFound, @@ -193,7 +193,7 @@ func TestRoutes(t *testing.T) { path: testProvider.TokenEndpoint().Relative(), values: map[string]string{ "grant_type": string(oidc.GrantTypeBearer), - "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(), + "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.String(), "assertion": jwtToken, }, wantCode: http.StatusBadRequest, @@ -206,7 +206,7 @@ func TestRoutes(t *testing.T) { basicAuth: &basicAuth{"web", "secret"}, values: map[string]string{ "grant_type": string(oidc.GrantTypeTokenExchange), - "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(), + "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.String(), "subject_token": jwtToken, "subject_token_type": string(oidc.AccessTokenType), }, @@ -223,7 +223,7 @@ func TestRoutes(t *testing.T) { basicAuth: &basicAuth{"sid1", "verysecret"}, values: map[string]string{ "grant_type": string(oidc.GrantTypeClientCredentials), - "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(), + "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.String(), }, wantCode: http.StatusOK, contains: []string{`{"access_token":"`, `","token_type":"Bearer","expires_in":299}`}, @@ -338,7 +338,7 @@ func TestRoutes(t *testing.T) { path: testProvider.DeviceAuthorizationEndpoint().Relative(), basicAuth: &basicAuth{"web", "secret"}, values: map[string]string{ - "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(), + "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.String(), }, wantCode: http.StatusOK, contains: []string{ diff --git a/pkg/op/session.go b/pkg/op/session.go index fd914d1..2467b20 100644 --- a/pkg/op/session.go +++ b/pkg/op/session.go @@ -8,6 +8,7 @@ import ( httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" + "golang.org/x/exp/slog" ) type SessionEnder interface { @@ -15,6 +16,7 @@ type SessionEnder interface { Storage() Storage IDTokenHintVerifier(context.Context) *IDTokenHintVerifier DefaultLogoutRedirectURI() string + Logger() *slog.Logger } func endSessionHandler(ender SessionEnder) func(http.ResponseWriter, *http.Request) { @@ -31,12 +33,12 @@ func EndSession(w http.ResponseWriter, r *http.Request, ender SessionEnder) { } session, err := ValidateEndSessionRequest(r.Context(), req, ender) if err != nil { - RequestError(w, r, err) + RequestError(w, r, err, ender.Logger()) return } err = ender.Storage().TerminateSession(r.Context(), session.UserID, session.ClientID) if err != nil { - RequestError(w, r, oidc.DefaultToServerError(err, "error terminating session")) + RequestError(w, r, oidc.DefaultToServerError(err, "error terminating session"), ender.Logger()) return } http.Redirect(w, r, session.RedirectURI, http.StatusFound) diff --git a/pkg/op/token_client_credentials.go b/pkg/op/token_client_credentials.go index 0cf7796..043bb07 100644 --- a/pkg/op/token_client_credentials.go +++ b/pkg/op/token_client_credentials.go @@ -14,18 +14,18 @@ import ( func ClientCredentialsExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { request, err := ParseClientCredentialsRequest(r, exchanger.Decoder()) if err != nil { - RequestError(w, r, err) + RequestError(w, r, err, exchanger.Logger()) } validatedRequest, client, err := ValidateClientCredentialsRequest(r.Context(), request, exchanger) if err != nil { - RequestError(w, r, err) + RequestError(w, r, err, exchanger.Logger()) return } resp, err := CreateClientCredentialsTokenResponse(r.Context(), validatedRequest, exchanger, client) if err != nil { - RequestError(w, r, err) + RequestError(w, r, err, exchanger.Logger()) return } diff --git a/pkg/op/token_code.go b/pkg/op/token_code.go index b5e892a..baf377b 100644 --- a/pkg/op/token_code.go +++ b/pkg/op/token_code.go @@ -13,20 +13,20 @@ import ( func CodeExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { tokenReq, err := ParseAccessTokenRequest(r, exchanger.Decoder()) if err != nil { - RequestError(w, r, err) + RequestError(w, r, err, exchanger.Logger()) } if tokenReq.Code == "" { - RequestError(w, r, oidc.ErrInvalidRequest().WithDescription("code missing")) + RequestError(w, r, oidc.ErrInvalidRequest().WithDescription("code missing"), exchanger.Logger()) return } authReq, client, err := ValidateAccessTokenRequest(r.Context(), tokenReq, exchanger) if err != nil { - RequestError(w, r, err) + RequestError(w, r, err, exchanger.Logger()) return } resp, err := CreateTokenResponse(r.Context(), authReq, client, exchanger, true, tokenReq.Code, "") if err != nil { - RequestError(w, r, err) + RequestError(w, r, err, exchanger.Logger()) return } httphelper.MarshalJSON(w, resp) diff --git a/pkg/op/token_exchange.go b/pkg/op/token_exchange.go index 93aa9b2..21db134 100644 --- a/pkg/op/token_exchange.go +++ b/pkg/op/token_exchange.go @@ -136,17 +136,17 @@ func (r *tokenExchangeRequest) SetSubject(subject string) { func TokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { tokenExchangeReq, clientID, clientSecret, err := ParseTokenExchangeRequest(r, exchanger.Decoder()) if err != nil { - RequestError(w, r, err) + RequestError(w, r, err, exchanger.Logger()) } tokenExchangeRequest, client, err := ValidateTokenExchangeRequest(r.Context(), tokenExchangeReq, clientID, clientSecret, exchanger) if err != nil { - RequestError(w, r, err) + RequestError(w, r, err, exchanger.Logger()) return } resp, err := CreateTokenExchangeResponse(r.Context(), tokenExchangeRequest, client, exchanger) if err != nil { - RequestError(w, r, err) + RequestError(w, r, err, exchanger.Logger()) return } httphelper.MarshalJSON(w, resp) diff --git a/pkg/op/token_jwt_profile.go b/pkg/op/token_jwt_profile.go index 4cd7b1e..357200e 100644 --- a/pkg/op/token_jwt_profile.go +++ b/pkg/op/token_jwt_profile.go @@ -18,23 +18,23 @@ type JWTAuthorizationGrantExchanger interface { func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizationGrantExchanger) { profileRequest, err := ParseJWTProfileGrantRequest(r, exchanger.Decoder()) if err != nil { - RequestError(w, r, err) + RequestError(w, r, err, exchanger.Logger()) } tokenRequest, err := VerifyJWTAssertion(r.Context(), profileRequest.Assertion, exchanger.JWTProfileVerifier(r.Context())) if err != nil { - RequestError(w, r, err) + RequestError(w, r, err, exchanger.Logger()) return } tokenRequest.Scopes, err = exchanger.Storage().ValidateJWTProfileScopes(r.Context(), tokenRequest.Issuer, profileRequest.Scope) if err != nil { - RequestError(w, r, err) + RequestError(w, r, err, exchanger.Logger()) return } resp, err := CreateJWTTokenResponse(r.Context(), tokenRequest, exchanger) if err != nil { - RequestError(w, r, err) + RequestError(w, r, err, exchanger.Logger()) return } httphelper.MarshalJSON(w, resp) diff --git a/pkg/op/token_refresh.go b/pkg/op/token_refresh.go index aeaa5b4..9421033 100644 --- a/pkg/op/token_refresh.go +++ b/pkg/op/token_refresh.go @@ -26,16 +26,16 @@ type RefreshTokenRequest interface { func RefreshTokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { tokenReq, err := ParseRefreshTokenRequest(r, exchanger.Decoder()) if err != nil { - RequestError(w, r, err) + RequestError(w, r, err, exchanger.Logger()) } validatedRequest, client, err := ValidateRefreshTokenRequest(r.Context(), tokenReq, exchanger) if err != nil { - RequestError(w, r, err) + RequestError(w, r, err, exchanger.Logger()) return } resp, err := CreateTokenResponse(r.Context(), validatedRequest, client, exchanger, true, "", tokenReq.RefreshToken) if err != nil { - RequestError(w, r, err) + RequestError(w, r, err, exchanger.Logger()) return } httphelper.MarshalJSON(w, resp) diff --git a/pkg/op/token_request.go b/pkg/op/token_request.go index c06a51b..0df2fce 100644 --- a/pkg/op/token_request.go +++ b/pkg/op/token_request.go @@ -7,6 +7,7 @@ import ( httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" + "golang.org/x/exp/slog" ) type Exchanger interface { @@ -22,6 +23,7 @@ type Exchanger interface { GrantTypeDeviceCodeSupported() bool AccessTokenVerifier(context.Context) *AccessTokenVerifier IDTokenHintVerifier(context.Context) *IDTokenHintVerifier + Logger() *slog.Logger } func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Request) { @@ -63,10 +65,10 @@ func Exchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { return } case "": - RequestError(w, r, oidc.ErrInvalidRequest().WithDescription("grant_type missing")) + RequestError(w, r, oidc.ErrInvalidRequest().WithDescription("grant_type missing"), exchanger.Logger()) return } - RequestError(w, r, oidc.ErrUnsupportedGrantType().WithDescription("%s not supported", grantType)) + RequestError(w, r, oidc.ErrUnsupportedGrantType().WithDescription("%s not supported", grantType), exchanger.Logger()) } // AuthenticatedTokenRequest is a helper interface for ParseAuthenticatedTokenRequest From 5ade1cd9de3a3ecb81fb8ccf933dbd791ec736f0 Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Thu, 31 Aug 2023 02:47:17 -0700 Subject: [PATCH 276/502] feat: add typ:JWT header to tokens (#435) --- pkg/op/signer.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pkg/op/signer.go b/pkg/op/signer.go index 22ef8ca..7e488f6 100644 --- a/pkg/op/signer.go +++ b/pkg/op/signer.go @@ -6,9 +6,7 @@ import ( "gopkg.in/square/go-jose.v2" ) -var ( - ErrSignerCreationFailed = errors.New("signer creation failed") -) +var ErrSignerCreationFailed = errors.New("signer creation failed") type SigningKey interface { SignatureAlgorithm() jose.SignatureAlgorithm @@ -23,9 +21,9 @@ func SignerFromKey(key SigningKey) (jose.Signer, error) { Key: key.Key(), KeyID: key.ID(), }, - }, &jose.SignerOptions{}) + }, (&jose.SignerOptions{}).WithType("JWT")) if err != nil { - return nil, ErrSignerCreationFailed //TODO: log / wrap error? + return nil, ErrSignerCreationFailed // TODO: log / wrap error? } return signer, nil } From 1683b319aeaa7b15d43b306f1774154cd94000a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 1 Sep 2023 11:53:14 +0300 Subject: [PATCH 277/502] feat(op): add opentelemetry to token endpoint (#436) * feat(op): add opentelemetry to token endpoint * drop go 1.18, add 1.21, do not fail fast --- .github/workflows/release.yml | 3 ++- README.md | 6 +++--- go.mod | 7 ++++++- go.sum | 11 +++++++++++ pkg/op/auth_request.go | 4 ++++ pkg/op/device.go | 4 ++++ pkg/op/op.go | 8 ++++++++ pkg/op/token.go | 14 ++++++++++++++ pkg/op/token_client_credentials.go | 13 +++++++++++++ pkg/op/token_code.go | 13 +++++++++++++ pkg/op/token_exchange.go | 4 ++++ pkg/op/token_jwt_profile.go | 7 +++++++ pkg/op/token_refresh.go | 10 ++++++++++ pkg/op/token_request.go | 12 +++++++++++- pkg/op/verifier_jwt_profile.go | 3 +++ 15 files changed, 113 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b21e7e9..2b3dac4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,8 +15,9 @@ jobs: test: runs-on: ubuntu-20.04 strategy: + fail-fast: false matrix: - go: ['1.18', '1.19', '1.20'] + go: ['1.19', '1.20', '1.21'] name: Go ${{ matrix.go }} test steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index e5a992f..0cf5274 100644 --- a/README.md +++ b/README.md @@ -115,10 +115,10 @@ Versions that also build are marked with :warning:. | Version | Supported | | ------- | ------------------ | -| <1.18 | :x: | -| 1.18 | :warning: | -| 1.19 | :white_check_mark: | +| <1.19 | :x: | +| 1.19 | :warning: | | 1.20 | :white_check_mark: | +| 1.21 | :white_check_mark: | ## Why another library diff --git a/go.mod b/go.mod index 6abc07b..a1b1714 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/zitadel/oidc/v2 -go 1.18 +go 1.19 require ( github.com/golang/mock v1.6.0 @@ -14,6 +14,8 @@ require ( github.com/rs/cors v1.9.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 + go.opentelemetry.io/otel v1.17.0 + go.opentelemetry.io/otel/trace v1.17.0 golang.org/x/oauth2 v0.11.0 golang.org/x/text v0.12.0 gopkg.in/square/go-jose.v2 v2.6.0 @@ -21,9 +23,12 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/otel/metric v1.17.0 // indirect golang.org/x/crypto v0.12.0 // indirect golang.org/x/net v0.14.0 // indirect golang.org/x/sys v0.11.0 // indirect diff --git a/go.sum b/go.sum index be1233b..83d02bb 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,11 @@ 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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -44,6 +49,12 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opentelemetry.io/otel v1.17.0 h1:MW+phZ6WZ5/uk2nd93ANk/6yJ+dVrvNWUjGhnnFU5jM= +go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0= +go.opentelemetry.io/otel/metric v1.17.0 h1:iG6LGVz5Gh+IuO0jmgvpTB6YVrCGngi8QGm+pMd8Pdc= +go.opentelemetry.io/otel/metric v1.17.0/go.mod h1:h4skoxdZI17AxwITdmdZjjYJQH5nzijUUjm+wtPph5o= +go.opentelemetry.io/otel/trace v1.17.0 h1:/SWhSRHmDPOImIAetP1QAeMnZYiQXrTy4fMMYOdSKWQ= +go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 5621951..5845756 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -464,6 +464,10 @@ func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthReques // AuthResponseToken creates the successful token(s) authentication response func AuthResponseToken(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer, client Client) { + ctx, span := tracer.Start(r.Context(), "AuthResponseToken") + defer span.End() + r = r.WithContext(ctx) + createAccessToken := authReq.GetResponseType() != oidc.ResponseTypeIDTokenOnly resp, err := CreateTokenResponse(r.Context(), authReq, client, authorizer, createAccessToken, "", "") if err != nil { diff --git a/pkg/op/device.go b/pkg/op/device.go index f584c31..2d3a8c9 100644 --- a/pkg/op/device.go +++ b/pkg/op/device.go @@ -196,6 +196,10 @@ func (r *deviceAccessTokenRequest) GetScopes() []string { } func DeviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { + ctx, span := tracer.Start(r.Context(), "DeviceAccessToken") + defer span.End() + r = r.WithContext(ctx) + if err := deviceAccessToken(w, r, exchanger); err != nil { RequestError(w, r, err) } diff --git a/pkg/op/op.go b/pkg/op/op.go index fc5262a..484149e 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -9,6 +9,8 @@ import ( "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/rs/cors" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" "golang.org/x/text/language" "gopkg.in/square/go-jose.v2" @@ -67,6 +69,12 @@ var ( } ) +var tracer trace.Tracer + +func init() { + tracer = otel.Tracer("github.com/zitadel/oidc/pkg/op") +} + type OpenIDProvider interface { Configuration Storage() Storage diff --git a/pkg/op/token.go b/pkg/op/token.go index 6dfc993..ae82b06 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -28,6 +28,9 @@ type AccessTokenClient interface { } func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Client, creator TokenCreator, createAccessToken bool, code, refreshToken string) (*oidc.AccessTokenResponse, error) { + ctx, span := tracer.Start(ctx, "CreateTokenResponse") + defer span.End() + var accessToken, newRefreshToken string var validity time.Duration if createAccessToken { @@ -84,6 +87,9 @@ func needsRefreshToken(tokenRequest TokenRequest, client AccessTokenClient) bool } func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTokenType AccessTokenType, creator TokenCreator, client AccessTokenClient, refreshToken string) (accessToken, newRefreshToken string, validity time.Duration, err error) { + ctx, span := tracer.Start(ctx, "CreateAccessToken") + defer span.End() + id, newRefreshToken, exp, err := createTokens(ctx, tokenRequest, creator.Storage(), refreshToken, client) if err != nil { return "", "", 0, err @@ -97,7 +103,9 @@ func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTok accessToken, err = CreateJWT(ctx, IssuerFromContext(ctx), tokenRequest, exp, id, client, creator.Storage()) return } + _, span = tracer.Start(ctx, "CreateBearerToken") accessToken, err = CreateBearerToken(id, tokenRequest.GetSubject(), creator.Crypto()) + span.End() return } @@ -106,6 +114,9 @@ func CreateBearerToken(tokenID, subject string, crypto Crypto) (string, error) { } func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, exp time.Time, id string, client AccessTokenClient, storage Storage) (string, error) { + ctx, span := tracer.Start(ctx, "CreateJWT") + defer span.End() + claims := oidc.NewAccessTokenClaims(issuer, tokenRequest.GetSubject(), tokenRequest.GetAudience(), exp, id, client.GetID(), client.ClockSkew()) if client != nil { restrictedScopes := client.RestrictAdditionalAccessTokenScopes()(tokenRequest.GetScopes()) @@ -152,6 +163,9 @@ type IDTokenRequest interface { } func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, validity time.Duration, accessToken, code string, storage Storage, client Client) (string, error) { + ctx, span := tracer.Start(ctx, "CreateIDToken") + defer span.End() + exp := time.Now().UTC().Add(client.ClockSkew()).Add(validity) var acr, nonce string if authRequest, ok := request.(AuthRequest); ok { diff --git a/pkg/op/token_client_credentials.go b/pkg/op/token_client_credentials.go index fc31d57..0b91a36 100644 --- a/pkg/op/token_client_credentials.go +++ b/pkg/op/token_client_credentials.go @@ -12,6 +12,10 @@ import ( // ClientCredentialsExchange handles the OAuth 2.0 client_credentials grant, including // parsing, validating, authorizing the client and finally returning a token func ClientCredentialsExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { + ctx, span := tracer.Start(r.Context(), "ClientCredentialsExchange") + defer span.End() + r = r.WithContext(ctx) + request, err := ParseClientCredentialsRequest(r, exchanger.Decoder()) if err != nil { RequestError(w, r, err) @@ -66,6 +70,9 @@ func ParseClientCredentialsRequest(r *http.Request, decoder httphelper.Decoder) // ValidateClientCredentialsRequest validates the client_credentials request parameters including authorization check of the client // and returns a TokenRequest and Client implementation to be used in the client_credentials response, resp. creation of the corresponding access_token. func ValidateClientCredentialsRequest(ctx context.Context, request *oidc.ClientCredentialsRequest, exchanger Exchanger) (TokenRequest, Client, error) { + ctx, span := tracer.Start(ctx, "ValidateClientCredentialsRequest") + defer span.End() + storage, ok := exchanger.Storage().(ClientCredentialsStorage) if !ok { return nil, nil, oidc.ErrUnsupportedGrantType().WithDescription("client_credentials grant not supported") @@ -85,6 +92,9 @@ func ValidateClientCredentialsRequest(ctx context.Context, request *oidc.ClientC } func AuthorizeClientCredentialsClient(ctx context.Context, request *oidc.ClientCredentialsRequest, storage ClientCredentialsStorage) (Client, error) { + ctx, span := tracer.Start(ctx, "AuthorizeClientCredentialsClient") + defer span.End() + client, err := storage.ClientCredentials(ctx, request.ClientID, request.ClientSecret) if err != nil { return nil, oidc.ErrInvalidClient().WithParent(err) @@ -98,6 +108,9 @@ func AuthorizeClientCredentialsClient(ctx context.Context, request *oidc.ClientC } func CreateClientCredentialsTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator, client Client) (*oidc.AccessTokenResponse, error) { + ctx, span := tracer.Start(ctx, "CreateClientCredentialsTokenResponse") + defer span.End() + accessToken, _, validity, err := CreateAccessToken(ctx, tokenRequest, client.AccessTokenType(), creator, client, "") if err != nil { return nil, err diff --git a/pkg/op/token_code.go b/pkg/op/token_code.go index 565a477..f2162ef 100644 --- a/pkg/op/token_code.go +++ b/pkg/op/token_code.go @@ -11,6 +11,10 @@ import ( // CodeExchange handles the OAuth 2.0 authorization_code grant, including // parsing, validating, authorizing the client and finally exchanging the code for tokens func CodeExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { + ctx, span := tracer.Start(r.Context(), "CodeExchange") + defer span.End() + r = r.WithContext(ctx) + tokenReq, err := ParseAccessTokenRequest(r, exchanger.Decoder()) if err != nil { RequestError(w, r, err) @@ -45,6 +49,9 @@ func ParseAccessTokenRequest(r *http.Request, decoder httphelper.Decoder) (*oidc // ValidateAccessTokenRequest validates the token request parameters including authorization check of the client // and returns the previous created auth request corresponding to the auth code func ValidateAccessTokenRequest(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (AuthRequest, Client, error) { + ctx, span := tracer.Start(ctx, "ValidateAccessTokenRequest") + defer span.End() + authReq, client, err := AuthorizeCodeClient(ctx, tokenReq, exchanger) if err != nil { return nil, nil, err @@ -64,6 +71,9 @@ func ValidateAccessTokenRequest(ctx context.Context, tokenReq *oidc.AccessTokenR // AuthorizeCodeClient checks the authorization of the client and that the used method was the one previously registered. // It than returns the auth request corresponding to the auth code func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (request AuthRequest, client Client, err error) { + ctx, span := tracer.Start(ctx, "AuthorizeCodeClient") + defer span.End() + if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion { jwtExchanger, ok := exchanger.(JWTAuthorizationGrantExchanger) if !ok || !exchanger.AuthMethodPrivateKeyJWTSupported() { @@ -104,6 +114,9 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, // AuthRequestByCode returns the AuthRequest previously created from Storage corresponding to the auth code or an error func AuthRequestByCode(ctx context.Context, storage Storage, code string) (AuthRequest, error) { + ctx, span := tracer.Start(ctx, "AuthRequestByCode") + defer span.End() + authReq, err := storage.AuthRequestByCode(ctx, code) if err != nil { return nil, oidc.ErrInvalidGrant().WithDescription("invalid code").WithParent(err) diff --git a/pkg/op/token_exchange.go b/pkg/op/token_exchange.go index 5a2387d..4f1ed43 100644 --- a/pkg/op/token_exchange.go +++ b/pkg/op/token_exchange.go @@ -134,6 +134,10 @@ func (r *tokenExchangeRequest) SetSubject(subject string) { // TokenExchange handles the OAuth 2.0 token exchange grant ("urn:ietf:params:oauth:grant-type:token-exchange") func TokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { + ctx, span := tracer.Start(r.Context(), "TokenExchange") + defer span.End() + r = r.WithContext(ctx) + tokenExchangeReq, clientID, clientSecret, err := ParseTokenExchangeRequest(r, exchanger.Decoder()) if err != nil { RequestError(w, r, err) diff --git a/pkg/op/token_jwt_profile.go b/pkg/op/token_jwt_profile.go index 23bac9a..5a94d99 100644 --- a/pkg/op/token_jwt_profile.go +++ b/pkg/op/token_jwt_profile.go @@ -16,6 +16,10 @@ type JWTAuthorizationGrantExchanger interface { // JWTProfile handles the OAuth 2.0 JWT Profile Authorization Grant https://tools.ietf.org/html/rfc7523#section-2.1 func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizationGrantExchanger) { + ctx, span := tracer.Start(r.Context(), "JWTProfile") + defer span.End() + r = r.WithContext(ctx) + profileRequest, err := ParseJWTProfileGrantRequest(r, exchanger.Decoder()) if err != nil { RequestError(w, r, err) @@ -56,6 +60,9 @@ func ParseJWTProfileGrantRequest(r *http.Request, decoder httphelper.Decoder) (* // CreateJWTTokenResponse creates an access_token response for a JWT Profile Grant request // by default the access_token is an opaque string, but can be specified by implementing the JWTProfileTokenStorage interface func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator) (*oidc.AccessTokenResponse, error) { + ctx, span := tracer.Start(ctx, "CreateJWTTokenResponse") + defer span.End() + // return an opaque token as default to not break current implementations tokenType := AccessTokenTypeBearer diff --git a/pkg/op/token_refresh.go b/pkg/op/token_refresh.go index 148d2a4..efb6fc0 100644 --- a/pkg/op/token_refresh.go +++ b/pkg/op/token_refresh.go @@ -24,6 +24,10 @@ type RefreshTokenRequest interface { // RefreshTokenExchange handles the OAuth 2.0 refresh_token grant, including // parsing, validating, authorizing the client and finally exchanging the refresh_token for new tokens func RefreshTokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { + ctx, span := tracer.Start(r.Context(), "RefreshTokenExchange") + defer span.End() + r = r.WithContext(ctx) + tokenReq, err := ParseRefreshTokenRequest(r, exchanger.Decoder()) if err != nil { RequestError(w, r, err) @@ -54,6 +58,9 @@ func ParseRefreshTokenRequest(r *http.Request, decoder httphelper.Decoder) (*oid // ValidateRefreshTokenRequest validates the refresh_token request parameters including authorization check of the client // and returns the data representing the original auth request corresponding to the refresh_token func ValidateRefreshTokenRequest(ctx context.Context, tokenReq *oidc.RefreshTokenRequest, exchanger Exchanger) (RefreshTokenRequest, Client, error) { + ctx, span := tracer.Start(ctx, "ValidateRefreshTokenRequest") + defer span.End() + if tokenReq.RefreshToken == "" { return nil, nil, oidc.ErrInvalidRequest().WithDescription("refresh_token missing") } @@ -89,6 +96,9 @@ func ValidateRefreshTokenScopes(requestedScopes []string, authRequest RefreshTok // AuthorizeRefreshClient checks the authorization of the client and that the used method was the one previously registered. // It than returns the data representing the original auth request corresponding to the refresh_token func AuthorizeRefreshClient(ctx context.Context, tokenReq *oidc.RefreshTokenRequest, exchanger Exchanger) (request RefreshTokenRequest, client Client, err error) { + ctx, span := tracer.Start(ctx, "AuthorizeRefreshClient") + defer span.End() + if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion { jwtExchanger, ok := exchanger.(JWTAuthorizationGrantExchanger) if !ok || !exchanger.AuthMethodPrivateKeyJWTSupported() { diff --git a/pkg/op/token_request.go b/pkg/op/token_request.go index b9e9805..1207375 100644 --- a/pkg/op/token_request.go +++ b/pkg/op/token_request.go @@ -26,7 +26,10 @@ type Exchanger interface { func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - Exchange(w, r, exchanger) + ctx, span := tracer.Start(r.Context(), "tokenHandler") + defer span.End() + + Exchange(w, r.WithContext(ctx), exchanger) } } @@ -79,6 +82,10 @@ type AuthenticatedTokenRequest interface { // ParseAuthenticatedTokenRequest parses the client_id and client_secret from the HTTP request from either // HTTP Basic Auth header or form body and sets them into the provided authenticatedTokenRequest interface func ParseAuthenticatedTokenRequest(r *http.Request, decoder httphelper.Decoder, request AuthenticatedTokenRequest) error { + ctx, span := tracer.Start(r.Context(), "ParseAuthenticatedTokenRequest") + defer span.End() + r = r.WithContext(ctx) + err := r.ParseForm() if err != nil { return oidc.ErrInvalidRequest().WithDescription("error parsing form").WithParent(err) @@ -128,6 +135,9 @@ func AuthorizeCodeChallenge(tokenReq *oidc.AccessTokenRequest, challenge *oidc.C // AuthorizePrivateJWTKey authorizes a client by validating the client_assertion's signature with a previously // registered public key (JWT Profile) func AuthorizePrivateJWTKey(ctx context.Context, clientAssertion string, exchanger JWTAuthorizationGrantExchanger) (Client, error) { + ctx, span := tracer.Start(ctx, "AuthorizePrivateJWTKey") + defer span.End() + jwtReq, err := VerifyJWTAssertion(ctx, clientAssertion, exchanger.JWTProfileVerifier(ctx)) if err != nil { return nil, err diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index 4d83c59..e7c9611 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -74,6 +74,9 @@ func (v *jwtProfileVerifier) CheckSubject(request *oidc.JWTTokenRequest) error { // // checks audience, exp, iat, signature and that issuer and sub are the same func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerifier) (*oidc.JWTTokenRequest, error) { + ctx, span := tracer.Start(ctx, "VerifyJWTAssertion") + defer span.End() + request := new(oidc.JWTTokenRequest) payload, err := oidc.ParseToken(assertion, request) if err != nil { From daf82a5e041cb1a2279d7e6f8f56d64dd58b4371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 1 Sep 2023 14:33:16 +0300 Subject: [PATCH 278/502] chore(deps): migrage jose to go-jose/v3 (#433) closes #390 --- example/server/storage/storage.go | 2 +- example/server/storage/storage_dynamic.go | 2 +- go.mod | 8 ++++---- go.sum | 19 +++++++++++-------- internal/testutil/token.go | 2 +- pkg/client/client.go | 2 +- pkg/client/profile/jwt_profile.go | 2 +- pkg/client/rp/jwks.go | 2 +- pkg/client/rp/relying_party.go | 2 +- pkg/client/rp/verifier.go | 2 +- pkg/client/rp/verifier_test.go | 2 +- pkg/crypto/hash.go | 2 +- pkg/crypto/sign.go | 2 +- pkg/oidc/keyset.go | 6 +++--- pkg/oidc/keyset_test.go | 2 +- pkg/oidc/token.go | 2 +- pkg/oidc/token_request.go | 2 +- pkg/oidc/token_test.go | 2 +- pkg/oidc/types.go | 2 +- pkg/oidc/verifier.go | 2 +- pkg/op/discovery.go | 2 +- pkg/op/discovery_test.go | 2 +- pkg/op/keys.go | 2 +- pkg/op/keys_test.go | 2 +- pkg/op/mock/authorizer.mock.impl.go | 2 +- pkg/op/mock/discovery.mock.go | 2 +- pkg/op/mock/signer.mock.go | 2 +- pkg/op/mock/storage.mock.go | 2 +- pkg/op/op.go | 2 +- pkg/op/signer.go | 2 +- pkg/op/storage.go | 2 +- pkg/op/verifier_jwt_profile.go | 2 +- 32 files changed, 47 insertions(+), 44 deletions(-) diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index e1160b6..56f96ce 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -11,8 +11,8 @@ import ( "sync" "time" + jose "github.com/go-jose/go-jose/v3" "github.com/google/uuid" - "gopkg.in/square/go-jose.v2" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" diff --git a/example/server/storage/storage_dynamic.go b/example/server/storage/storage_dynamic.go index 0d99aa2..5deb5dc 100644 --- a/example/server/storage/storage_dynamic.go +++ b/example/server/storage/storage_dynamic.go @@ -4,7 +4,7 @@ import ( "context" "time" - "gopkg.in/square/go-jose.v2" + jose "github.com/go-jose/go-jose/v3" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" diff --git a/go.mod b/go.mod index 62aa39b..1fe66cc 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( github.com/go-chi/chi v1.5.4 + github.com/go-jose/go-jose/v3 v3.0.0 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 github.com/google/uuid v1.3.0 @@ -17,8 +18,7 @@ require ( github.com/zitadel/schema v1.3.0 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 golang.org/x/oauth2 v0.7.0 - golang.org/x/text v0.9.0 - gopkg.in/square/go-jose.v2 v2.6.0 + golang.org/x/text v0.10.0 ) require ( @@ -27,8 +27,8 @@ require ( github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.7.0 // indirect - golang.org/x/net v0.9.0 // indirect + golang.org/x/crypto v0.10.0 // indirect + golang.org/x/net v0.11.0 // indirect golang.org/x/sys v0.11.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.29.1 // indirect diff --git a/go.sum b/go.sum index 9c44f0f..9524f82 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ 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/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -10,6 +12,7 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= @@ -41,6 +44,7 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -52,9 +56,10 @@ github.com/zitadel/logging v0.4.0/go.mod h1:6uALRJawpkkuUPCkgzfgcPR3c2N908wqnOnI github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= +golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -64,8 +69,8 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= +golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= @@ -83,8 +88,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -102,8 +107,6 @@ google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= -gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/testutil/token.go b/internal/testutil/token.go index 41778de..2dd788f 100644 --- a/internal/testutil/token.go +++ b/internal/testutil/token.go @@ -8,9 +8,9 @@ import ( "errors" "time" + jose "github.com/go-jose/go-jose/v3" "github.com/muhlemmer/gu" "github.com/zitadel/oidc/v3/pkg/oidc" - "gopkg.in/square/go-jose.v2" ) // KeySet implements oidc.Keys diff --git a/pkg/client/client.go b/pkg/client/client.go index 7b76dfd..d7764f6 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -11,8 +11,8 @@ import ( "strings" "time" + jose "github.com/go-jose/go-jose/v3" "golang.org/x/oauth2" - "gopkg.in/square/go-jose.v2" "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/crypto" diff --git a/pkg/client/profile/jwt_profile.go b/pkg/client/profile/jwt_profile.go index 419f417..a24033c 100644 --- a/pkg/client/profile/jwt_profile.go +++ b/pkg/client/profile/jwt_profile.go @@ -5,8 +5,8 @@ import ( "net/http" "time" + jose "github.com/go-jose/go-jose/v3" "golang.org/x/oauth2" - "gopkg.in/square/go-jose.v2" "github.com/zitadel/oidc/v3/pkg/client" "github.com/zitadel/oidc/v3/pkg/oidc" diff --git a/pkg/client/rp/jwks.go b/pkg/client/rp/jwks.go index 79cf232..28aec9b 100644 --- a/pkg/client/rp/jwks.go +++ b/pkg/client/rp/jwks.go @@ -7,7 +7,7 @@ import ( "net/http" "sync" - "gopkg.in/square/go-jose.v2" + jose "github.com/go-jose/go-jose/v3" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 34cdb39..877c837 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -9,11 +9,11 @@ import ( "net/url" "time" + jose "github.com/go-jose/go-jose/v3" "github.com/google/uuid" "github.com/zitadel/logging" "golang.org/x/exp/slog" "golang.org/x/oauth2" - "gopkg.in/square/go-jose.v2" "github.com/zitadel/oidc/v3/pkg/client" httphelper "github.com/zitadel/oidc/v3/pkg/http" diff --git a/pkg/client/rp/verifier.go b/pkg/client/rp/verifier.go index 3294f40..adf8872 100644 --- a/pkg/client/rp/verifier.go +++ b/pkg/client/rp/verifier.go @@ -4,7 +4,7 @@ import ( "context" "time" - "gopkg.in/square/go-jose.v2" + jose "github.com/go-jose/go-jose/v3" "github.com/zitadel/oidc/v3/pkg/oidc" ) diff --git a/pkg/client/rp/verifier_test.go b/pkg/client/rp/verifier_test.go index 11bf2f9..3e6d9d9 100644 --- a/pkg/client/rp/verifier_test.go +++ b/pkg/client/rp/verifier_test.go @@ -5,11 +5,11 @@ import ( "testing" "time" + jose "github.com/go-jose/go-jose/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" tu "github.com/zitadel/oidc/v3/internal/testutil" "github.com/zitadel/oidc/v3/pkg/oidc" - "gopkg.in/square/go-jose.v2" ) func TestVerifyTokens(t *testing.T) { diff --git a/pkg/crypto/hash.go b/pkg/crypto/hash.go index 6fcc71f..0ed2774 100644 --- a/pkg/crypto/hash.go +++ b/pkg/crypto/hash.go @@ -8,7 +8,7 @@ import ( "fmt" "hash" - "gopkg.in/square/go-jose.v2" + jose "github.com/go-jose/go-jose/v3" ) var ErrUnsupportedAlgorithm = errors.New("unsupported signing algorithm") diff --git a/pkg/crypto/sign.go b/pkg/crypto/sign.go index a0b9cae..d6c002b 100644 --- a/pkg/crypto/sign.go +++ b/pkg/crypto/sign.go @@ -4,7 +4,7 @@ import ( "encoding/json" "errors" - "gopkg.in/square/go-jose.v2" + jose "github.com/go-jose/go-jose/v3" ) func Sign(object interface{}, signer jose.Signer) (string, error) { diff --git a/pkg/oidc/keyset.go b/pkg/oidc/keyset.go index c6e865b..a4a5a1c 100644 --- a/pkg/oidc/keyset.go +++ b/pkg/oidc/keyset.go @@ -7,7 +7,7 @@ import ( "crypto/rsa" "errors" - "gopkg.in/square/go-jose.v2" + jose "github.com/go-jose/go-jose/v3" ) const ( @@ -46,8 +46,8 @@ func GetKeyIDAndAlg(jws *jose.JSONWebSignature) (string, string) { // // will return false none or multiple match // -//deprecated: use FindMatchingKey which will return an error (more specific) instead of just a bool -//moved implementation already to FindMatchingKey +// deprecated: use FindMatchingKey which will return an error (more specific) instead of just a bool +// moved implementation already to FindMatchingKey func FindKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (jose.JSONWebKey, bool) { key, err := FindMatchingKey(keyID, use, expectedAlg, keys...) return key, err == nil diff --git a/pkg/oidc/keyset_test.go b/pkg/oidc/keyset_test.go index 82b3ee8..f8641f2 100644 --- a/pkg/oidc/keyset_test.go +++ b/pkg/oidc/keyset_test.go @@ -7,7 +7,7 @@ import ( "reflect" "testing" - "gopkg.in/square/go-jose.v2" + jose "github.com/go-jose/go-jose/v3" ) func TestFindKey(t *testing.T) { diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index c02eaf4..4624e50 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -5,8 +5,8 @@ import ( "os" "time" + jose "github.com/go-jose/go-jose/v3" "golang.org/x/oauth2" - "gopkg.in/square/go-jose.v2" "github.com/muhlemmer/gu" "github.com/zitadel/oidc/v3/pkg/crypto" diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index 5c5cf20..330c0c2 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "gopkg.in/square/go-jose.v2" + jose "github.com/go-jose/go-jose/v3" ) const ( diff --git a/pkg/oidc/token_test.go b/pkg/oidc/token_test.go index ef1e77f..854f455 100644 --- a/pkg/oidc/token_test.go +++ b/pkg/oidc/token_test.go @@ -4,9 +4,9 @@ import ( "testing" "time" + jose "github.com/go-jose/go-jose/v3" "github.com/stretchr/testify/assert" "golang.org/x/text/language" - "gopkg.in/square/go-jose.v2" ) var ( diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index 5db8bad..dd604ad 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -8,9 +8,9 @@ import ( "strings" "time" + jose "github.com/go-jose/go-jose/v3" "github.com/zitadel/schema" "golang.org/x/text/language" - "gopkg.in/square/go-jose.v2" ) type Audience []string diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go index 2d4e7a6..14174a4 100644 --- a/pkg/oidc/verifier.go +++ b/pkg/oidc/verifier.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "gopkg.in/square/go-jose.v2" + jose "github.com/go-jose/go-jose/v3" str "github.com/zitadel/oidc/v3/pkg/strings" ) diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 38afeab..782a279 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -4,7 +4,7 @@ import ( "context" "net/http" - "gopkg.in/square/go-jose.v2" + jose "github.com/go-jose/go-jose/v3" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" diff --git a/pkg/op/discovery_test.go b/pkg/op/discovery_test.go index e55e905..3e95ec3 100644 --- a/pkg/op/discovery_test.go +++ b/pkg/op/discovery_test.go @@ -6,10 +6,10 @@ import ( "net/http/httptest" "testing" + jose "github.com/go-jose/go-jose/v3" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gopkg.in/square/go-jose.v2" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" diff --git a/pkg/op/keys.go b/pkg/op/keys.go index 418dcb5..fe111f0 100644 --- a/pkg/op/keys.go +++ b/pkg/op/keys.go @@ -4,7 +4,7 @@ import ( "context" "net/http" - "gopkg.in/square/go-jose.v2" + jose "github.com/go-jose/go-jose/v3" httphelper "github.com/zitadel/oidc/v3/pkg/http" ) diff --git a/pkg/op/keys_test.go b/pkg/op/keys_test.go index 259b87c..e1a3851 100644 --- a/pkg/op/keys_test.go +++ b/pkg/op/keys_test.go @@ -7,9 +7,9 @@ import ( "net/http/httptest" "testing" + jose "github.com/go-jose/go-jose/v3" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" - "gopkg.in/square/go-jose.v2" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" diff --git a/pkg/op/mock/authorizer.mock.impl.go b/pkg/op/mock/authorizer.mock.impl.go index 4d66a92..ba5082f 100644 --- a/pkg/op/mock/authorizer.mock.impl.go +++ b/pkg/op/mock/authorizer.mock.impl.go @@ -4,9 +4,9 @@ import ( "context" "testing" + jose "github.com/go-jose/go-jose/v3" "github.com/golang/mock/gomock" "github.com/zitadel/schema" - "gopkg.in/square/go-jose.v2" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" diff --git a/pkg/op/mock/discovery.mock.go b/pkg/op/mock/discovery.mock.go index 4c33953..c5d3d3a 100644 --- a/pkg/op/mock/discovery.mock.go +++ b/pkg/op/mock/discovery.mock.go @@ -8,8 +8,8 @@ import ( context "context" reflect "reflect" + jose "github.com/go-jose/go-jose/v3" gomock "github.com/golang/mock/gomock" - jose "gopkg.in/square/go-jose.v2" ) // MockDiscoverStorage is a mock of DiscoverStorage interface. diff --git a/pkg/op/mock/signer.mock.go b/pkg/op/mock/signer.mock.go index 7075241..15718e0 100644 --- a/pkg/op/mock/signer.mock.go +++ b/pkg/op/mock/signer.mock.go @@ -7,8 +7,8 @@ package mock import ( reflect "reflect" + jose "github.com/go-jose/go-jose/v3" gomock "github.com/golang/mock/gomock" - jose "gopkg.in/square/go-jose.v2" ) // MockSigningKey is a mock of SigningKey interface. diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index 6bfb1c9..a1ce598 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -9,10 +9,10 @@ import ( reflect "reflect" time "time" + jose "github.com/go-jose/go-jose/v3" gomock "github.com/golang/mock/gomock" oidc "github.com/zitadel/oidc/v3/pkg/oidc" op "github.com/zitadel/oidc/v3/pkg/op" - jose "gopkg.in/square/go-jose.v2" ) // MockStorage is a mock of Storage interface. diff --git a/pkg/op/op.go b/pkg/op/op.go index d8ae570..0175d7f 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -7,11 +7,11 @@ import ( "time" "github.com/go-chi/chi" + jose "github.com/go-jose/go-jose/v3" "github.com/rs/cors" "github.com/zitadel/schema" "golang.org/x/exp/slog" "golang.org/x/text/language" - "gopkg.in/square/go-jose.v2" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" diff --git a/pkg/op/signer.go b/pkg/op/signer.go index 22ef8ca..37bd5bb 100644 --- a/pkg/op/signer.go +++ b/pkg/op/signer.go @@ -3,7 +3,7 @@ package op import ( "errors" - "gopkg.in/square/go-jose.v2" + jose "github.com/go-jose/go-jose/v3" ) var ( diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 23d2133..aca1615 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -5,7 +5,7 @@ import ( "errors" "time" - "gopkg.in/square/go-jose.v2" + jose "github.com/go-jose/go-jose/v3" "github.com/zitadel/oidc/v3/pkg/oidc" ) diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index 1daa15f..19adbb6 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -6,7 +6,7 @@ import ( "fmt" "time" - "gopkg.in/square/go-jose.v2" + jose "github.com/go-jose/go-jose/v3" "github.com/zitadel/oidc/v3/pkg/oidc" ) From 52a7fff3145c7c3a02d9e39b30a2d38dd7c0bbcd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 00:46:36 +0300 Subject: [PATCH 279/502] chore(deps): bump actions/checkout from 3 to 4 (#439) Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/release.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d2bae79..a8106ae 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2b3dac4..52478b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: go: ['1.19', '1.20', '1.21'] name: Go ${{ matrix.go }} test steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup go uses: actions/setup-go@v4 with: @@ -38,7 +38,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Source checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Semantic Release uses: cycjimmy/semantic-release-action@v3 with: From 0bc75d86ffcaaaf2ca232eb71b68ca15b84c181e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 00:47:21 +0300 Subject: [PATCH 280/502] chore(deps): bump cycjimmy/semantic-release-action from 3 to 4 (#438) Bumps [cycjimmy/semantic-release-action](https://github.com/cycjimmy/semantic-release-action) from 3 to 4. - [Release notes](https://github.com/cycjimmy/semantic-release-action/releases) - [Changelog](https://github.com/cycjimmy/semantic-release-action/blob/main/docs/CHANGELOG.md) - [Commits](https://github.com/cycjimmy/semantic-release-action/compare/v3...v4) --- updated-dependencies: - dependency-name: cycjimmy/semantic-release-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 52478b2..644b23f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,7 +40,7 @@ jobs: - name: Source checkout uses: actions/checkout@v4 - name: Semantic Release - uses: cycjimmy/semantic-release-action@v3 + uses: cycjimmy/semantic-release-action@v4 with: dry_run: false semantic_version: 18.0.1 From 61f1925f516b786a0c536a68bc27402a4b586485 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 14:52:18 +0300 Subject: [PATCH 281/502] chore(deps): bump github.com/rs/cors from 1.9.0 to 1.10.0 (#442) Bumps [github.com/rs/cors](https://github.com/rs/cors) from 1.9.0 to 1.10.0. - [Release notes](https://github.com/rs/cors/releases) - [Commits](https://github.com/rs/cors/compare/v1.9.0...v1.10.0) --- updated-dependencies: - dependency-name: github.com/rs/cors dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a1b1714..8e75e79 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/gorilla/securecookie v1.1.1 github.com/jeremija/gosubmit v0.2.7 github.com/muhlemmer/gu v0.3.1 - github.com/rs/cors v1.9.0 + github.com/rs/cors v1.10.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 go.opentelemetry.io/otel v1.17.0 diff --git a/go.sum b/go.sum index 83d02bb..e4ec96d 100644 --- a/go.sum +++ b/go.sum @@ -40,8 +40,8 @@ github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= -github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.10.0 h1:62NOS1h+r8p1mW6FM0FSB0exioXLhd/sh15KpjWBZ+8= +github.com/rs/cors v1.10.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From 607a76c15408c667cfd5718284db02d84c93a607 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 14:53:08 +0300 Subject: [PATCH 282/502] chore(deps): bump golang.org/x/oauth2 from 0.11.0 to 0.12.0 (#441) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.11.0 to 0.12.0. - [Commits](https://github.com/golang/oauth2/compare/v0.11.0...v0.12.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 8e75e79..a8cd590 100644 --- a/go.mod +++ b/go.mod @@ -16,8 +16,8 @@ require ( github.com/stretchr/testify v1.8.4 go.opentelemetry.io/otel v1.17.0 go.opentelemetry.io/otel/trace v1.17.0 - golang.org/x/oauth2 v0.11.0 - golang.org/x/text v0.12.0 + golang.org/x/oauth2 v0.12.0 + golang.org/x/text v0.13.0 gopkg.in/square/go-jose.v2 v2.6.0 ) @@ -29,9 +29,9 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/otel/metric v1.17.0 // indirect - golang.org/x/crypto v0.12.0 // indirect - golang.org/x/net v0.14.0 // indirect - golang.org/x/sys v0.11.0 // indirect + golang.org/x/crypto v0.13.0 // indirect + golang.org/x/net v0.15.0 // indirect + golang.org/x/sys v0.12.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index e4ec96d..e5102a8 100644 --- a/go.sum +++ b/go.sum @@ -57,8 +57,8 @@ go.opentelemetry.io/otel/trace v1.17.0 h1:/SWhSRHmDPOImIAetP1QAeMnZYiQXrTy4fMMYO go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -66,11 +66,11 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= -golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= +golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= +golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -79,14 +79,14 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= From 364a7591d640298b478511c7d4589bfc8ccd7622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Thu, 7 Sep 2023 15:25:39 +0300 Subject: [PATCH 283/502] feat: issuer from Forwarded header (#443) --- go.mod | 2 +- go.sum | 6 +-- pkg/op/config.go | 34 ++++++++++++++ pkg/op/config_test.go | 106 +++++++++++++++++++++++++++++++++++++++++- pkg/op/op.go | 7 +++ 5 files changed, 149 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index a8cd590..250edd5 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/gorilla/securecookie v1.1.1 github.com/jeremija/gosubmit v0.2.7 github.com/muhlemmer/gu v0.3.1 + github.com/muhlemmer/httpforwarded v0.1.0 github.com/rs/cors v1.10.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 @@ -34,6 +35,5 @@ require ( golang.org/x/sys v0.12.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e5102a8..7cd9691 100644 --- a/go.sum +++ b/go.sum @@ -32,12 +32,11 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc= github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= +github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= +github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/cors v1.10.0 h1:62NOS1h+r8p1mW6FM0FSB0exioXLhd/sh15KpjWBZ+8= @@ -103,7 +102,6 @@ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/op/config.go b/pkg/op/config.go index c40ed39..e5789f7 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -2,10 +2,12 @@ package op import ( "errors" + "log" "net/http" "net/url" "strings" + "github.com/muhlemmer/httpforwarded" "golang.org/x/text/language" ) @@ -52,6 +54,21 @@ type Configuration interface { type IssuerFromRequest func(r *http.Request) string func IssuerFromHost(path string) func(bool) (IssuerFromRequest, error) { + return issuerFromForwardedOrHost(path, false) +} + +// IssuerFromForwardedOrHost tries to establish the Issuer based +// on the Forwarded header host field. +// If multiple Forwarded headers are present, the first mention +// of the host field will be used. +// If the Forwarded header is not present, no host field is found, +// or there is a parser error the Request Host will be used as a fallback. +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded +func IssuerFromForwardedOrHost(path string) func(bool) (IssuerFromRequest, error) { + return issuerFromForwardedOrHost(path, true) +} + +func issuerFromForwardedOrHost(path string, parseForwarded bool) func(bool) (IssuerFromRequest, error) { return func(allowInsecure bool) (IssuerFromRequest, error) { issuerPath, err := url.Parse(path) if err != nil { @@ -61,11 +78,28 @@ func IssuerFromHost(path string) func(bool) (IssuerFromRequest, error) { return nil, err } return func(r *http.Request) string { + if parseForwarded { + if host, ok := hostFromForwarded(r); ok { + return dynamicIssuer(host, path, allowInsecure) + } + } return dynamicIssuer(r.Host, path, allowInsecure) }, nil } } +func hostFromForwarded(r *http.Request) (host string, ok bool) { + fwd, err := httpforwarded.ParseFromRequest(r) + if err != nil { + log.Printf("Err: issuer from forwarded header: %v", err) // TODO change to slog on next branch + return "", false + } + if fwd == nil || len(fwd["host"]) == 0 { + return "", false + } + return fwd["host"][0], true +} + func StaticIssuer(issuer string) func(bool) (IssuerFromRequest, error) { return func(allowInsecure bool) (IssuerFromRequest, error) { if err := ValidateIssuer(issuer, allowInsecure); err != nil { diff --git a/pkg/op/config_test.go b/pkg/op/config_test.go index cfe4e61..dcafc3a 100644 --- a/pkg/op/config_test.go +++ b/pkg/op/config_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestValidateIssuer(t *testing.T) { @@ -234,7 +235,7 @@ func TestIssuerFromHost(t *testing.T) { }, }, { - "custom path unsecure", + "custom path insecure", args{ path: "/custom/", allowInsecure: true, @@ -261,6 +262,109 @@ func TestIssuerFromHost(t *testing.T) { } } +func TestIssuerFromForwardedOrHost(t *testing.T) { + type args struct { + path string + target string + forwarded []string + } + type res struct { + issuer string + } + tests := []struct { + name string + args args + res res + }{ + { + "header parse error", + args{ + path: "/custom/", + target: "https://issuer.com", + forwarded: []string{"~~~"}, + }, + res{ + issuer: "https://issuer.com/custom/", + }, + }, + { + "no forwarded header", + args{ + path: "/custom/", + target: "https://issuer.com", + }, + res{ + issuer: "https://issuer.com/custom/", + }, + }, + // by=;for=;host=;proto= + { + "forwarded header without host", + args{ + path: "/custom/", + target: "https://issuer.com", + forwarded: []string{ + `by=identifier;for=identifier;proto=https`, + }, + }, + res{ + issuer: "https://issuer.com/custom/", + }, + }, + { + "forwarded header with host", + args{ + path: "/custom/", + target: "https://issuer.com", + forwarded: []string{ + `by=identifier;for=identifier;host=first.com;proto=https`, + }, + }, + res{ + issuer: "https://first.com/custom/", + }, + }, + { + "forwarded header with multiple hosts", + args{ + path: "/custom/", + target: "https://issuer.com", + forwarded: []string{ + `by=identifier;for=identifier;host=first.com;proto=https,host=second.com`, + }, + }, + res{ + issuer: "https://first.com/custom/", + }, + }, + { + "multiple forwarded headers hosts", + args{ + path: "/custom/", + target: "https://issuer.com", + forwarded: []string{ + `by=identifier;for=identifier;host=first.com;proto=https,host=second.com`, + `by=identifier;for=identifier;host=third.com;proto=https`, + }, + }, + res{ + issuer: "https://first.com/custom/", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + issuer, err := IssuerFromForwardedOrHost(tt.args.path)(false) + require.NoError(t, err) + req := httptest.NewRequest("", tt.args.target, nil) + if tt.args.forwarded != nil { + req.Header["Forwarded"] = tt.args.forwarded + } + assert.Equal(t, tt.res.issuer, issuer(req)) + }) + } +} + func TestStaticIssuer(t *testing.T) { type args struct { issuer string diff --git a/pkg/op/op.go b/pkg/op/op.go index 484149e..c4be14f 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -169,10 +169,17 @@ func NewOpenIDProvider(issuer string, config *Config, storage Storage, opOpts .. return newProvider(config, storage, StaticIssuer(issuer), opOpts...) } +// NewForwardedOpenIDProvider tries to establishes the issuer from the request Host. func NewDynamicOpenIDProvider(path string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) { return newProvider(config, storage, IssuerFromHost(path), opOpts...) } +// NewForwardedOpenIDProvider tries to establish the Issuer from a Forwarded request header, if it is set. +// See [IssuerFromForwardedOrHost] for details. +func NewForwardedOpenIDProvider(path string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) { + return newProvider(config, storage, IssuerFromForwardedOrHost(path), opOpts...) +} + func newProvider(config *Config, storage Storage, issuer func(bool) (IssuerFromRequest, error), opOpts ...Option) (_ *Provider, err error) { o := &Provider{ config: config, From 47cd8f376d190398bce14462d4dfdacadb1b77f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Sep 2023 16:12:20 +0300 Subject: [PATCH 284/502] chore(deps): bump go.opentelemetry.io/otel from 1.17.0 to 1.18.0 (#445) Bumps [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) from 1.17.0 to 1.18.0. - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.17.0...v1.18.0) --- updated-dependencies: - dependency-name: go.opentelemetry.io/otel dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 250edd5..5923722 100644 --- a/go.mod +++ b/go.mod @@ -15,8 +15,8 @@ require ( github.com/rs/cors v1.10.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 - go.opentelemetry.io/otel v1.17.0 - go.opentelemetry.io/otel/trace v1.17.0 + go.opentelemetry.io/otel v1.18.0 + go.opentelemetry.io/otel/trace v1.18.0 golang.org/x/oauth2 v0.12.0 golang.org/x/text v0.13.0 gopkg.in/square/go-jose.v2 v2.6.0 @@ -29,7 +29,7 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel/metric v1.17.0 // indirect + go.opentelemetry.io/otel/metric v1.18.0 // indirect golang.org/x/crypto v0.13.0 // indirect golang.org/x/net v0.15.0 // indirect golang.org/x/sys v0.12.0 // indirect diff --git a/go.sum b/go.sum index 7cd9691..2b0e678 100644 --- a/go.sum +++ b/go.sum @@ -48,12 +48,12 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.opentelemetry.io/otel v1.17.0 h1:MW+phZ6WZ5/uk2nd93ANk/6yJ+dVrvNWUjGhnnFU5jM= -go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0= -go.opentelemetry.io/otel/metric v1.17.0 h1:iG6LGVz5Gh+IuO0jmgvpTB6YVrCGngi8QGm+pMd8Pdc= -go.opentelemetry.io/otel/metric v1.17.0/go.mod h1:h4skoxdZI17AxwITdmdZjjYJQH5nzijUUjm+wtPph5o= -go.opentelemetry.io/otel/trace v1.17.0 h1:/SWhSRHmDPOImIAetP1QAeMnZYiQXrTy4fMMYOdSKWQ= -go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY= +go.opentelemetry.io/otel v1.18.0 h1:TgVozPGZ01nHyDZxK5WGPFB9QexeTMXEH7+tIClWfzs= +go.opentelemetry.io/otel v1.18.0/go.mod h1:9lWqYO0Db579XzVuCKFNPDl4s73Voa+zEck3wHaAYQI= +go.opentelemetry.io/otel/metric v1.18.0 h1:JwVzw94UYmbx3ej++CwLUQZxEODDj/pOuTCvzhtRrSQ= +go.opentelemetry.io/otel/metric v1.18.0/go.mod h1:nNSpsVDjWGfb7chbRLUNW+PBNdcSTHD4Uu5pfFMOI0k= +go.opentelemetry.io/otel/trace v1.18.0 h1:NY+czwbHbmndxojTEKiSMHkG2ClNH2PwmcHrdo0JY10= +go.opentelemetry.io/otel/trace v1.18.0/go.mod h1:T2+SGJGuYZY3bjj5rgh/hN7KIrlpWC5nS8Mjvzckz+0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= From 0f8a0585bf29f1daf26bd17b3b899be7fb40a37f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Thu, 28 Sep 2023 17:30:08 +0300 Subject: [PATCH 285/502] feat(op): Server interface (#447) * first draft of a new server interface * allow any response type * complete interface docs * refelct the format from the proposal * intermediate commit with some methods implemented * implement remaining token grant type methods * implement remaining server methods * error handling * rewrite auth request validation * define handlers, routes * input validation and concrete handlers * check if client credential client is authenticated * copy and modify the routes test for the legacy server * run integration tests against both Server and Provider * remove unuse ValidateAuthRequestV2 function * unit tests for error handling * cleanup tokenHandler * move server routest test * unit test authorize * handle client credentials in VerifyClient * change code exchange route test * finish http unit tests * review server interface docs and spelling * add withClient unit test * server options * cleanup unused GrantType method * resolve typo comments * make endpoints pointers to enable/disable them * jwt profile base work * jwt: correct the test expect --------- Co-authored-by: Livio Spring --- example/server/exampleop/op.go | 9 +- example/server/main.go | 2 +- example/server/storage/client.go | 2 +- pkg/client/integration_test.go | 21 +- pkg/op/auth_request.go | 18 +- pkg/op/client.go | 7 + pkg/op/config.go | 16 +- pkg/op/device.go | 38 +- pkg/op/discovery.go | 39 +- pkg/op/discovery_test.go | 8 +- pkg/op/endpoint.go | 30 +- pkg/op/endpoint_test.go | 31 +- pkg/op/error.go | 101 +++ pkg/op/error_test.go | 400 +++++++++ pkg/op/mock/configuration.mock.go | 32 +- pkg/op/op.go | 67 +- pkg/op/op_test.go | 51 ++ pkg/op/probes.go | 4 +- pkg/op/server.go | 346 ++++++++ pkg/op/server_http.go | 480 +++++++++++ pkg/op/server_http_routes_test.go | 345 ++++++++ pkg/op/server_http_test.go | 1333 +++++++++++++++++++++++++++++ pkg/op/server_legacy.go | 344 ++++++++ pkg/op/server_test.go | 5 + pkg/op/token_code.go | 2 +- pkg/op/token_exchange.go | 36 +- pkg/op/token_request.go | 6 +- pkg/op/token_revocation.go | 7 +- 28 files changed, 3654 insertions(+), 126 deletions(-) create mode 100644 pkg/op/server.go create mode 100644 pkg/op/server_http.go create mode 100644 pkg/op/server_http_routes_test.go create mode 100644 pkg/op/server_http_test.go create mode 100644 pkg/op/server_legacy.go create mode 100644 pkg/op/server_test.go diff --git a/example/server/exampleop/op.go b/example/server/exampleop/op.go index b5ee7b3..f1906ba 100644 --- a/example/server/exampleop/op.go +++ b/example/server/exampleop/op.go @@ -40,7 +40,7 @@ var counter atomic.Int64 // SetupServer creates an OIDC server with Issuer=http://localhost: // // Use one of the pre-made clients in storage/clients.go or register a new one. -func SetupServer(issuer string, storage Storage, logger *slog.Logger) chi.Router { +func SetupServer(issuer string, storage Storage, logger *slog.Logger, wrapServer bool) chi.Router { // the OpenID Provider requires a 32-byte key for (token) encryption // be sure to create a proper crypto random key and manage it securely! key := sha256.Sum256([]byte("test")) @@ -77,12 +77,17 @@ func SetupServer(issuer string, storage Storage, logger *slog.Logger) chi.Router registerDeviceAuth(storage, r) }) + handler := http.Handler(provider) + if wrapServer { + handler = op.NewLegacyServer(provider, *op.DefaultEndpoints) + } + // we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration) // is served on the correct path // // if your issuer ends with a path (e.g. http://localhost:9998/custom/path/), // then you would have to set the path prefix (/custom/path/) - router.Mount("/", provider) + router.Mount("/", handler) return router } diff --git a/example/server/main.go b/example/server/main.go index a1cc461..38057fb 100644 --- a/example/server/main.go +++ b/example/server/main.go @@ -27,7 +27,7 @@ func main() { Level: slog.LevelDebug, }), ) - router := exampleop.SetupServer(issuer, storage, logger) + router := exampleop.SetupServer(issuer, storage, logger, false) server := &http.Server{ Addr: ":" + port, diff --git a/example/server/storage/client.go b/example/server/storage/client.go index a3e7cc4..f512a99 100644 --- a/example/server/storage/client.go +++ b/example/server/storage/client.go @@ -185,7 +185,7 @@ func WebClient(id, secret string, redirectURIs ...string) *Client { authMethod: oidc.AuthMethodBasic, loginURL: defaultLoginURL, responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode}, - grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken}, + grantTypes: oidc.AllGrantTypes, accessTokenType: op.AccessTokenTypeBearer, devMode: false, idTokenUserinfoClaimsAssertion: false, diff --git a/pkg/client/integration_test.go b/pkg/client/integration_test.go index 7cbb62e..1d3559e 100644 --- a/pkg/client/integration_test.go +++ b/pkg/client/integration_test.go @@ -3,6 +3,7 @@ package client_test import ( "bytes" "context" + "fmt" "io" "math/rand" "net/http" @@ -50,6 +51,14 @@ func TestMain(m *testing.M) { } func TestRelyingPartySession(t *testing.T) { + for _, wrapServer := range []bool{false, true} { + t.Run(fmt.Sprint("wrapServer ", wrapServer), func(t *testing.T) { + testRelyingPartySession(t, wrapServer) + }) + } +} + +func testRelyingPartySession(t *testing.T, wrapServer bool) { t.Log("------- start example OP ------") targetURL := "http://local-site" exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL)) @@ -57,7 +66,7 @@ func TestRelyingPartySession(t *testing.T) { opServer := httptest.NewServer(&dh) defer opServer.Close() t.Logf("auth server at %s", opServer.URL) - dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger) + dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger, wrapServer) seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano())) clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25) @@ -101,6 +110,14 @@ func TestRelyingPartySession(t *testing.T) { } func TestResourceServerTokenExchange(t *testing.T) { + for _, wrapServer := range []bool{false, true} { + t.Run(fmt.Sprint("wrapServer ", wrapServer), func(t *testing.T) { + testResourceServerTokenExchange(t, wrapServer) + }) + } +} + +func testResourceServerTokenExchange(t *testing.T, wrapServer bool) { t.Log("------- start example OP ------") targetURL := "http://local-site" exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL)) @@ -108,7 +125,7 @@ func TestResourceServerTokenExchange(t *testing.T) { opServer := httptest.NewServer(&dh) defer opServer.Close() t.Logf("auth server at %s", opServer.URL) - dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger) + dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger, wrapServer) seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano())) clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25) diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 7610248..20b1bf4 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -74,7 +74,7 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { } ctx := r.Context() if authReq.RequestParam != "" && authorizer.RequestObjectSupported() { - authReq, err = ParseRequestObject(ctx, authReq, authorizer.Storage(), IssuerFromContext(ctx)) + err = ParseRequestObject(ctx, authReq, authorizer.Storage(), IssuerFromContext(ctx)) if err != nil { AuthRequestError(w, r, authReq, err, authorizer) return @@ -130,31 +130,31 @@ func ParseAuthorizeRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.A // ParseRequestObject parse the `request` parameter, validates the token including the signature // and copies the token claims into the auth request -func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, issuer string) (*oidc.AuthRequest, error) { +func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, issuer string) error { requestObject := new(oidc.RequestObject) payload, err := oidc.ParseToken(authReq.RequestParam, requestObject) if err != nil { - return nil, err + return err } if requestObject.ClientID != "" && requestObject.ClientID != authReq.ClientID { - return authReq, oidc.ErrInvalidRequest() + return oidc.ErrInvalidRequest() } if requestObject.ResponseType != "" && requestObject.ResponseType != authReq.ResponseType { - return authReq, oidc.ErrInvalidRequest() + return oidc.ErrInvalidRequest() } if requestObject.Issuer != requestObject.ClientID { - return authReq, oidc.ErrInvalidRequest() + return oidc.ErrInvalidRequest() } if !str.Contains(requestObject.Audience, issuer) { - return authReq, oidc.ErrInvalidRequest() + return oidc.ErrInvalidRequest() } keySet := &jwtProfileKeySet{storage: storage, clientID: requestObject.Issuer} if err = oidc.CheckSignature(ctx, authReq.RequestParam, payload, requestObject, nil, keySet); err != nil { - return authReq, err + return err } CopyRequestObjectToAuthRequest(authReq, requestObject) - return authReq, nil + return nil } // CopyRequestObjectToAuthRequest overwrites present values from the Request Object into the auth request diff --git a/pkg/op/client.go b/pkg/op/client.go index d01845f..04ef3c7 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -180,3 +180,10 @@ func ClientIDFromRequest(r *http.Request, p ClientProvider) (clientID string, au } return data.ClientID, false, nil } + +type ClientCredentials struct { + ClientID string `schema:"client_id"` + ClientSecret string `schema:"client_secret"` // Client secret from Basic auth or request body + ClientAssertion string `schema:"client_assertion"` // JWT + ClientAssertionType string `schema:"client_assertion_type"` +} diff --git a/pkg/op/config.go b/pkg/op/config.go index c40ed39..f61412a 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -20,14 +20,14 @@ var ( type Configuration interface { IssuerFromRequest(r *http.Request) string Insecure() bool - AuthorizationEndpoint() Endpoint - TokenEndpoint() Endpoint - IntrospectionEndpoint() Endpoint - UserinfoEndpoint() Endpoint - RevocationEndpoint() Endpoint - EndSessionEndpoint() Endpoint - KeysEndpoint() Endpoint - DeviceAuthorizationEndpoint() Endpoint + AuthorizationEndpoint() *Endpoint + TokenEndpoint() *Endpoint + IntrospectionEndpoint() *Endpoint + UserinfoEndpoint() *Endpoint + RevocationEndpoint() *Endpoint + EndSessionEndpoint() *Endpoint + KeysEndpoint() *Endpoint + DeviceAuthorizationEndpoint() *Endpoint AuthMethodPostSupported() bool CodeMethodS256Supported() bool diff --git a/pkg/op/device.go b/pkg/op/device.go index 029bed8..55d3c57 100644 --- a/pkg/op/device.go +++ b/pkg/op/device.go @@ -63,41 +63,51 @@ func DeviceAuthorizationHandler(o OpenIDProvider) func(http.ResponseWriter, *htt } func DeviceAuthorization(w http.ResponseWriter, r *http.Request, o OpenIDProvider) error { - storage, err := assertDeviceStorage(o.Storage()) - if err != nil { - return err - } - req, err := ParseDeviceCodeRequest(r, o) if err != nil { return err } + response, err := createDeviceAuthorization(r.Context(), req, req.ClientID, o) + if err != nil { + return err + } + httphelper.MarshalJSON(w, response) + return nil +} + +func createDeviceAuthorization(ctx context.Context, req *oidc.DeviceAuthorizationRequest, clientID string, o OpenIDProvider) (*oidc.DeviceAuthorizationResponse, error) { + storage, err := assertDeviceStorage(o.Storage()) + if err != nil { + return nil, err + } config := o.DeviceAuthorization() deviceCode, err := NewDeviceCode(RecommendedDeviceCodeBytes) if err != nil { - return err + return nil, NewStatusError(err, http.StatusInternalServerError) } userCode, err := NewUserCode([]rune(config.UserCode.CharSet), config.UserCode.CharAmount, config.UserCode.DashInterval) if err != nil { - return err + return nil, NewStatusError(err, http.StatusInternalServerError) } expires := time.Now().Add(config.Lifetime) - err = storage.StoreDeviceAuthorization(r.Context(), req.ClientID, deviceCode, userCode, expires, req.Scopes) + err = storage.StoreDeviceAuthorization(ctx, clientID, deviceCode, userCode, expires, req.Scopes) if err != nil { - return err + return nil, NewStatusError(err, http.StatusInternalServerError) } var verification *url.URL if config.UserFormURL != "" { if verification, err = url.Parse(config.UserFormURL); err != nil { - return oidc.ErrServerError().WithParent(err).WithDescription("invalid URL for device user form") + err = oidc.ErrServerError().WithParent(err).WithDescription("invalid URL for device user form") + return nil, NewStatusError(err, http.StatusInternalServerError) } } else { - if verification, err = url.Parse(IssuerFromContext(r.Context())); err != nil { - return oidc.ErrServerError().WithParent(err).WithDescription("invalid URL for issuer") + if verification, err = url.Parse(IssuerFromContext(ctx)); err != nil { + err = oidc.ErrServerError().WithParent(err).WithDescription("invalid URL for issuer") + return nil, NewStatusError(err, http.StatusInternalServerError) } verification.Path = config.UserFormPath } @@ -112,9 +122,7 @@ func DeviceAuthorization(w http.ResponseWriter, r *http.Request, o OpenIDProvide verification.RawQuery = "user_code=" + userCode response.VerificationURIComplete = verification.String() - - httphelper.MarshalJSON(w, response) - return nil + return response, nil } func ParseDeviceCodeRequest(r *http.Request, o OpenIDProvider) (*oidc.DeviceAuthorizationRequest, error) { diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 782a279..8251261 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -25,7 +25,7 @@ var DefaultSupportedScopes = []string{ func discoveryHandler(c Configuration, s DiscoverStorage) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - Discover(w, CreateDiscoveryConfig(r, c, s)) + Discover(w, CreateDiscoveryConfig(r.Context(), c, s)) } } @@ -33,8 +33,8 @@ func Discover(w http.ResponseWriter, config *oidc.DiscoveryConfiguration) { httphelper.MarshalJSON(w, config) } -func CreateDiscoveryConfig(r *http.Request, config Configuration, storage DiscoverStorage) *oidc.DiscoveryConfiguration { - issuer := config.IssuerFromRequest(r) +func CreateDiscoveryConfig(ctx context.Context, config Configuration, storage DiscoverStorage) *oidc.DiscoveryConfiguration { + issuer := IssuerFromContext(ctx) return &oidc.DiscoveryConfiguration{ Issuer: issuer, AuthorizationEndpoint: config.AuthorizationEndpoint().Absolute(issuer), @@ -49,7 +49,38 @@ func CreateDiscoveryConfig(r *http.Request, config Configuration, storage Discov ResponseTypesSupported: ResponseTypes(config), GrantTypesSupported: GrantTypes(config), SubjectTypesSupported: SubjectTypes(config), - IDTokenSigningAlgValuesSupported: SigAlgorithms(r.Context(), storage), + IDTokenSigningAlgValuesSupported: SigAlgorithms(ctx, storage), + RequestObjectSigningAlgValuesSupported: RequestObjectSigAlgorithms(config), + TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(config), + TokenEndpointAuthSigningAlgValuesSupported: TokenSigAlgorithms(config), + IntrospectionEndpointAuthSigningAlgValuesSupported: IntrospectionSigAlgorithms(config), + IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(config), + RevocationEndpointAuthSigningAlgValuesSupported: RevocationSigAlgorithms(config), + RevocationEndpointAuthMethodsSupported: AuthMethodsRevocationEndpoint(config), + ClaimsSupported: SupportedClaims(config), + CodeChallengeMethodsSupported: CodeChallengeMethods(config), + UILocalesSupported: config.SupportedUILocales(), + RequestParameterSupported: config.RequestObjectSupported(), + } +} + +func createDiscoveryConfigV2(ctx context.Context, config Configuration, storage DiscoverStorage, endpoints *Endpoints) *oidc.DiscoveryConfiguration { + issuer := IssuerFromContext(ctx) + return &oidc.DiscoveryConfiguration{ + Issuer: issuer, + AuthorizationEndpoint: endpoints.Authorization.Absolute(issuer), + TokenEndpoint: endpoints.Token.Absolute(issuer), + IntrospectionEndpoint: endpoints.Introspection.Absolute(issuer), + UserinfoEndpoint: endpoints.Userinfo.Absolute(issuer), + RevocationEndpoint: endpoints.Revocation.Absolute(issuer), + EndSessionEndpoint: endpoints.EndSession.Absolute(issuer), + JwksURI: endpoints.JwksURI.Absolute(issuer), + DeviceAuthorizationEndpoint: endpoints.DeviceAuthorization.Absolute(issuer), + ScopesSupported: Scopes(config), + ResponseTypesSupported: ResponseTypes(config), + GrantTypesSupported: GrantTypes(config), + SubjectTypesSupported: SubjectTypes(config), + IDTokenSigningAlgValuesSupported: SigAlgorithms(ctx, storage), RequestObjectSigningAlgValuesSupported: RequestObjectSigAlgorithms(config), TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(config), TokenEndpointAuthSigningAlgValuesSupported: TokenSigAlgorithms(config), diff --git a/pkg/op/discovery_test.go b/pkg/op/discovery_test.go index 3e95ec3..84e1216 100644 --- a/pkg/op/discovery_test.go +++ b/pkg/op/discovery_test.go @@ -48,9 +48,9 @@ func TestDiscover(t *testing.T) { func TestCreateDiscoveryConfig(t *testing.T) { type args struct { - request *http.Request - c op.Configuration - s op.DiscoverStorage + ctx context.Context + c op.Configuration + s op.DiscoverStorage } tests := []struct { name string @@ -61,7 +61,7 @@ func TestCreateDiscoveryConfig(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := op.CreateDiscoveryConfig(tt.args.request, tt.args.c, tt.args.s) + got := op.CreateDiscoveryConfig(tt.args.ctx, tt.args.c, tt.args.s) assert.Equal(t, tt.want, got) }) } diff --git a/pkg/op/endpoint.go b/pkg/op/endpoint.go index b1e1507..1ac1cad 100644 --- a/pkg/op/endpoint.go +++ b/pkg/op/endpoint.go @@ -1,32 +1,46 @@ package op -import "strings" +import ( + "errors" + "strings" +) type Endpoint struct { path string url string } -func NewEndpoint(path string) Endpoint { - return Endpoint{path: path} +func NewEndpoint(path string) *Endpoint { + return &Endpoint{path: path} } -func NewEndpointWithURL(path, url string) Endpoint { - return Endpoint{path: path, url: url} +func NewEndpointWithURL(path, url string) *Endpoint { + return &Endpoint{path: path, url: url} } -func (e Endpoint) Relative() string { +func (e *Endpoint) Relative() string { + if e == nil { + return "" + } return relativeEndpoint(e.path) } -func (e Endpoint) Absolute(host string) string { +func (e *Endpoint) Absolute(host string) string { + if e == nil { + return "" + } if e.url != "" { return e.url } return absoluteEndpoint(host, e.path) } -func (e Endpoint) Validate() error { +var ErrNilEndpoint = errors.New("nil endpoint") + +func (e *Endpoint) Validate() error { + if e == nil { + return ErrNilEndpoint + } return nil // TODO: } diff --git a/pkg/op/endpoint_test.go b/pkg/op/endpoint_test.go index 46e5d47..bf112ef 100644 --- a/pkg/op/endpoint_test.go +++ b/pkg/op/endpoint_test.go @@ -3,13 +3,14 @@ package op_test import ( "testing" + "github.com/stretchr/testify/require" "github.com/zitadel/oidc/v3/pkg/op" ) func TestEndpoint_Path(t *testing.T) { tests := []struct { name string - e op.Endpoint + e *op.Endpoint want string }{ { @@ -27,6 +28,11 @@ func TestEndpoint_Path(t *testing.T) { op.NewEndpointWithURL("/test", "http://test.com/test"), "/test", }, + { + "nil", + nil, + "", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -43,7 +49,7 @@ func TestEndpoint_Absolute(t *testing.T) { } tests := []struct { name string - e op.Endpoint + e *op.Endpoint args args want string }{ @@ -77,6 +83,12 @@ func TestEndpoint_Absolute(t *testing.T) { args{"https://host"}, "https://test.com/test", }, + { + "nil", + nil, + args{"https://host"}, + "", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -91,16 +103,19 @@ func TestEndpoint_Absolute(t *testing.T) { func TestEndpoint_Validate(t *testing.T) { tests := []struct { name string - e op.Endpoint - wantErr bool + e *op.Endpoint + wantErr error }{ - // TODO: Add test cases. + { + "nil", + nil, + op.ErrNilEndpoint, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := tt.e.Validate(); (err != nil) != tt.wantErr { - t.Errorf("Endpoint.Validate() error = %v, wantErr %v", err, tt.wantErr) - } + err := tt.e.Validate() + require.ErrorIs(t, err, tt.wantErr) }) } } diff --git a/pkg/op/error.go b/pkg/op/error.go index 9981fec..0cac14b 100644 --- a/pkg/op/error.go +++ b/pkg/op/error.go @@ -1,6 +1,9 @@ package op import ( + "context" + "errors" + "fmt" "net/http" httphelper "github.com/zitadel/oidc/v3/pkg/http" @@ -66,3 +69,101 @@ func RequestError(w http.ResponseWriter, r *http.Request, err error, logger *slo logger.Log(r.Context(), e.LogLevel(), "request error", "oidc_error", e) httphelper.MarshalJSONWithStatus(w, e, status) } + +// TryErrorRedirect tries to handle an error by redirecting a client. +// If this attempt fails, an error is returned that must be returned +// to the client instead. +func TryErrorRedirect(ctx context.Context, authReq ErrAuthRequest, parent error, encoder httphelper.Encoder, logger *slog.Logger) (*Redirect, error) { + e := oidc.DefaultToServerError(parent, parent.Error()) + logger = logger.With("oidc_error", e) + + if authReq == nil { + logger.Log(ctx, e.LogLevel(), "auth request") + return nil, AsStatusError(e, http.StatusBadRequest) + } + + if logAuthReq, ok := authReq.(LogAuthRequest); ok { + logger = logger.With("auth_request", logAuthReq) + } + + if authReq.GetRedirectURI() == "" || e.IsRedirectDisabled() { + logger.Log(ctx, e.LogLevel(), "auth request: not redirecting") + return nil, AsStatusError(e, http.StatusBadRequest) + } + + e.State = authReq.GetState() + var responseMode oidc.ResponseMode + if rm, ok := authReq.(interface{ GetResponseMode() oidc.ResponseMode }); ok { + responseMode = rm.GetResponseMode() + } + url, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), responseMode, e, encoder) + if err != nil { + logger.ErrorContext(ctx, "auth response URL", "error", err) + return nil, AsStatusError(err, http.StatusBadRequest) + } + logger.Log(ctx, e.LogLevel(), "auth request redirect", "url", url) + return NewRedirect(url), nil +} + +// StatusError wraps an error with a HTTP status code. +// The status code is passed to the handler's writer. +type StatusError struct { + parent error + statusCode int +} + +// NewStatusError sets the parent and statusCode to a new StatusError. +// It is recommended for parent to be an [oidc.Error]. +// +// Typically implementations should only use this to signal something +// very specific, like an internal server error. +// If a returned error is not a StatusError, the framework +// will set a statusCode based on what the standard specifies, +// which is [http.StatusBadRequest] for most of the time. +// If the error encountered can described clearly with a [oidc.Error], +// do not use this function, as it might break standard rules! +func NewStatusError(parent error, statusCode int) StatusError { + return StatusError{ + parent: parent, + statusCode: statusCode, + } +} + +// AsStatusError unwraps a StatusError from err +// and returns it unmodified if found. +// If no StatuError was found, a new one is returned +// with statusCode set to it as a default. +func AsStatusError(err error, statusCode int) (target StatusError) { + if errors.As(err, &target) { + return target + } + return NewStatusError(err, statusCode) +} + +func (e StatusError) Error() string { + return fmt.Sprintf("%s: %s", http.StatusText(e.statusCode), e.parent.Error()) +} + +func (e StatusError) Unwrap() error { + return e.parent +} + +func (e StatusError) Is(err error) bool { + var target StatusError + if !errors.As(err, &target) { + return false + } + return errors.Is(e.parent, target.parent) && + e.statusCode == target.statusCode +} + +// WriteError asserts for a StatusError containing an [oidc.Error]. +// If no StatusError is found, the status code will default to [http.StatusBadRequest]. +// If no [oidc.Error] was found in the parent, the error type defaults to [oidc.ServerError]. +func WriteError(w http.ResponseWriter, r *http.Request, err error, logger *slog.Logger) { + statusError := AsStatusError(err, http.StatusBadRequest) + e := oidc.DefaultToServerError(statusError.parent, statusError.parent.Error()) + + logger.Log(r.Context(), e.LogLevel(), "request error", "oidc_error", e) + httphelper.MarshalJSONWithStatus(w, e, statusError.statusCode) +} diff --git a/pkg/op/error_test.go b/pkg/op/error_test.go index dc5ef11..689ee5a 100644 --- a/pkg/op/error_test.go +++ b/pkg/op/error_test.go @@ -1,9 +1,12 @@ package op import ( + "context" + "fmt" "io" "net/http" "net/http/httptest" + "net/url" "strings" "testing" @@ -275,3 +278,400 @@ func TestRequestError(t *testing.T) { }) } } + +func TestTryErrorRedirect(t *testing.T) { + type args struct { + ctx context.Context + authReq ErrAuthRequest + parent error + } + tests := []struct { + name string + args args + want *Redirect + wantErr error + wantLog string + }{ + { + name: "nil auth request", + args: args{ + ctx: context.Background(), + authReq: nil, + parent: io.ErrClosedPipe, + }, + wantErr: NewStatusError(io.ErrClosedPipe, http.StatusBadRequest), + wantLog: `{ + "level":"ERROR", + "msg":"auth request", + "time":"not", + "oidc_error":{ + "description":"io: read/write on closed pipe", + "parent":"io: read/write on closed pipe", + "type":"server_error" + } + }`, + }, + { + name: "auth request, no redirect URI", + args: args{ + ctx: context.Background(), + authReq: &oidc.AuthRequest{ + Scopes: oidc.SpaceDelimitedArray{"a", "b"}, + ResponseType: "responseType", + ClientID: "123", + State: "state1", + ResponseMode: oidc.ResponseModeQuery, + }, + parent: oidc.ErrInteractionRequired().WithDescription("sign in"), + }, + wantErr: NewStatusError(oidc.ErrInteractionRequired().WithDescription("sign in"), http.StatusBadRequest), + wantLog: `{ + "level":"WARN", + "msg":"auth request: not redirecting", + "time":"not", + "auth_request":{ + "client_id":"123", + "redirect_uri":"", + "response_type":"responseType", + "scopes":"a b" + }, + "oidc_error":{ + "description":"sign in", + "type":"interaction_required" + } + }`, + }, + { + name: "auth request, redirect disabled", + args: args{ + ctx: context.Background(), + authReq: &oidc.AuthRequest{ + Scopes: oidc.SpaceDelimitedArray{"a", "b"}, + ResponseType: "responseType", + ClientID: "123", + RedirectURI: "http://example.com/callback", + State: "state1", + ResponseMode: oidc.ResponseModeQuery, + }, + parent: oidc.ErrInvalidRequestRedirectURI().WithDescription("oops"), + }, + wantErr: NewStatusError(oidc.ErrInvalidRequestRedirectURI().WithDescription("oops"), http.StatusBadRequest), + wantLog: `{ + "level":"WARN", + "msg":"auth request: not redirecting", + "time":"not", + "auth_request":{ + "client_id":"123", + "redirect_uri":"http://example.com/callback", + "response_type":"responseType", + "scopes":"a b" + }, + "oidc_error":{ + "description":"oops", + "type":"invalid_request", + "redirect_disabled":true + } + }`, + }, + { + name: "auth request, url parse error", + args: args{ + ctx: context.Background(), + authReq: &oidc.AuthRequest{ + Scopes: oidc.SpaceDelimitedArray{"a", "b"}, + ResponseType: "responseType", + ClientID: "123", + RedirectURI: "can't parse this!\n", + State: "state1", + ResponseMode: oidc.ResponseModeQuery, + }, + parent: oidc.ErrInteractionRequired().WithDescription("sign in"), + }, + wantErr: func() error { + //lint:ignore SA1007 just recreating the error for testing + _, err := url.Parse("can't parse this!\n") + err = oidc.ErrServerError().WithParent(err) + return NewStatusError(err, http.StatusBadRequest) + }(), + wantLog: `{ + "level":"ERROR", + "msg":"auth response URL", + "time":"not", + "auth_request":{ + "client_id":"123", + "redirect_uri":"can't parse this!\n", + "response_type":"responseType", + "scopes":"a b" + }, + "error":{ + "type":"server_error", + "parent":"parse \"can't parse this!\\n\": net/url: invalid control character in URL" + }, + "oidc_error":{ + "description":"sign in", + "type":"interaction_required" + } + }`, + }, + { + name: "auth request redirect", + args: args{ + ctx: context.Background(), + authReq: &oidc.AuthRequest{ + Scopes: oidc.SpaceDelimitedArray{"a", "b"}, + ResponseType: "responseType", + ClientID: "123", + RedirectURI: "http://example.com/callback", + State: "state1", + ResponseMode: oidc.ResponseModeQuery, + }, + parent: oidc.ErrInteractionRequired().WithDescription("sign in"), + }, + want: &Redirect{ + URL: "http://example.com/callback?error=interaction_required&error_description=sign+in&state=state1", + }, + wantLog: `{ + "level":"WARN", + "msg":"auth request redirect", + "time":"not", + "auth_request":{ + "client_id":"123", + "redirect_uri":"http://example.com/callback", + "response_type":"responseType", + "scopes":"a b" + }, + "oidc_error":{ + "description":"sign in", + "type":"interaction_required" + }, + "url":"http://example.com/callback?error=interaction_required&error_description=sign+in&state=state1" + }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logOut := new(strings.Builder) + logger := slog.New( + slog.NewJSONHandler(logOut, &slog.HandlerOptions{ + Level: slog.LevelInfo, + }).WithAttrs([]slog.Attr{slog.String("time", "not")}), + ) + encoder := schema.NewEncoder() + + got, err := TryErrorRedirect(tt.args.ctx, tt.args.authReq, tt.args.parent, encoder, logger) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + + gotLog := logOut.String() + t.Log(gotLog) + assert.JSONEq(t, tt.wantLog, gotLog, "log output") + }) + } +} + +func TestNewStatusError(t *testing.T) { + err := NewStatusError(io.ErrClosedPipe, http.StatusInternalServerError) + + want := "Internal Server Error: io: read/write on closed pipe" + got := fmt.Sprint(err) + assert.Equal(t, want, got) +} + +func TestAsStatusError(t *testing.T) { + type args struct { + err error + statusCode int + } + tests := []struct { + name string + args args + want string + }{ + { + name: "already status error", + args: args{ + err: NewStatusError(io.ErrClosedPipe, http.StatusInternalServerError), + statusCode: http.StatusBadRequest, + }, + want: "Internal Server Error: io: read/write on closed pipe", + }, + { + name: "oidc error", + args: args{ + err: oidc.ErrAcrInvalid, + statusCode: http.StatusBadRequest, + }, + want: "Bad Request: acr is invalid", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := AsStatusError(tt.args.err, tt.args.statusCode) + got := fmt.Sprint(err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestStatusError_Unwrap(t *testing.T) { + err := NewStatusError(io.ErrClosedPipe, http.StatusInternalServerError) + require.ErrorIs(t, err, io.ErrClosedPipe) +} + +func TestStatusError_Is(t *testing.T) { + type args struct { + err error + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "nil error", + args: args{err: nil}, + want: false, + }, + { + name: "other error", + args: args{err: io.EOF}, + want: false, + }, + { + name: "other parent", + args: args{err: NewStatusError(io.EOF, http.StatusInternalServerError)}, + want: false, + }, + { + name: "other status", + args: args{err: NewStatusError(io.ErrClosedPipe, http.StatusInsufficientStorage)}, + want: false, + }, + { + name: "same", + args: args{err: NewStatusError(io.ErrClosedPipe, http.StatusInternalServerError)}, + want: true, + }, + { + name: "wrapped", + args: args{err: fmt.Errorf("wrap: %w", NewStatusError(io.ErrClosedPipe, http.StatusInternalServerError))}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := NewStatusError(io.ErrClosedPipe, http.StatusInternalServerError) + if got := e.Is(tt.args.err); got != tt.want { + t.Errorf("StatusError.Is() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestWriteError(t *testing.T) { + tests := []struct { + name string + err error + wantStatus int + wantBody string + wantLog string + }{ + { + name: "not a status or oidc error", + err: io.ErrClosedPipe, + wantStatus: http.StatusBadRequest, + wantBody: `{ + "error":"server_error", + "error_description":"io: read/write on closed pipe" + }`, + wantLog: `{ + "level":"ERROR", + "msg":"request error", + "oidc_error":{ + "description":"io: read/write on closed pipe", + "parent":"io: read/write on closed pipe", + "type":"server_error" + }, + "time":"not" + }`, + }, + { + name: "status error w/o oidc", + err: NewStatusError(io.ErrClosedPipe, http.StatusInternalServerError), + wantStatus: http.StatusInternalServerError, + wantBody: `{ + "error":"server_error", + "error_description":"io: read/write on closed pipe" + }`, + wantLog: `{ + "level":"ERROR", + "msg":"request error", + "oidc_error":{ + "description":"io: read/write on closed pipe", + "parent":"io: read/write on closed pipe", + "type":"server_error" + }, + "time":"not" + }`, + }, + { + name: "oidc error w/o status", + err: oidc.ErrInvalidRequest().WithDescription("oops"), + wantStatus: http.StatusBadRequest, + wantBody: `{ + "error":"invalid_request", + "error_description":"oops" + }`, + wantLog: `{ + "level":"WARN", + "msg":"request error", + "oidc_error":{ + "description":"oops", + "type":"invalid_request" + }, + "time":"not" + }`, + }, + { + name: "status with oidc error", + err: NewStatusError( + oidc.ErrUnauthorizedClient().WithDescription("oops"), + http.StatusUnauthorized, + ), + wantStatus: http.StatusUnauthorized, + wantBody: `{ + "error":"unauthorized_client", + "error_description":"oops" + }`, + wantLog: `{ + "level":"WARN", + "msg":"request error", + "oidc_error":{ + "description":"oops", + "type":"unauthorized_client" + }, + "time":"not" + }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logOut := new(strings.Builder) + logger := slog.New( + slog.NewJSONHandler(logOut, &slog.HandlerOptions{ + Level: slog.LevelInfo, + }).WithAttrs([]slog.Attr{slog.String("time", "not")}), + ) + r := httptest.NewRequest("GET", "/target", nil) + w := httptest.NewRecorder() + + WriteError(w, r, tt.err, logger) + res := w.Result() + assert.Equal(t, tt.wantStatus, res.StatusCode, "status code") + gotBody, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.JSONEq(t, tt.wantBody, string(gotBody), "body") + assert.JSONEq(t, tt.wantLog, logOut.String()) + }) + } +} diff --git a/pkg/op/mock/configuration.mock.go b/pkg/op/mock/configuration.mock.go index 96429dd..f392a45 100644 --- a/pkg/op/mock/configuration.mock.go +++ b/pkg/op/mock/configuration.mock.go @@ -65,10 +65,10 @@ func (mr *MockConfigurationMockRecorder) AuthMethodPrivateKeyJWTSupported() *gom } // AuthorizationEndpoint mocks base method. -func (m *MockConfiguration) AuthorizationEndpoint() op.Endpoint { +func (m *MockConfiguration) AuthorizationEndpoint() *op.Endpoint { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AuthorizationEndpoint") - ret0, _ := ret[0].(op.Endpoint) + ret0, _ := ret[0].(*op.Endpoint) return ret0 } @@ -107,10 +107,10 @@ func (mr *MockConfigurationMockRecorder) DeviceAuthorization() *gomock.Call { } // DeviceAuthorizationEndpoint mocks base method. -func (m *MockConfiguration) DeviceAuthorizationEndpoint() op.Endpoint { +func (m *MockConfiguration) DeviceAuthorizationEndpoint() *op.Endpoint { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeviceAuthorizationEndpoint") - ret0, _ := ret[0].(op.Endpoint) + ret0, _ := ret[0].(*op.Endpoint) return ret0 } @@ -121,10 +121,10 @@ func (mr *MockConfigurationMockRecorder) DeviceAuthorizationEndpoint() *gomock.C } // EndSessionEndpoint mocks base method. -func (m *MockConfiguration) EndSessionEndpoint() op.Endpoint { +func (m *MockConfiguration) EndSessionEndpoint() *op.Endpoint { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "EndSessionEndpoint") - ret0, _ := ret[0].(op.Endpoint) + ret0, _ := ret[0].(*op.Endpoint) return ret0 } @@ -233,10 +233,10 @@ func (mr *MockConfigurationMockRecorder) IntrospectionAuthMethodPrivateKeyJWTSup } // IntrospectionEndpoint mocks base method. -func (m *MockConfiguration) IntrospectionEndpoint() op.Endpoint { +func (m *MockConfiguration) IntrospectionEndpoint() *op.Endpoint { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "IntrospectionEndpoint") - ret0, _ := ret[0].(op.Endpoint) + ret0, _ := ret[0].(*op.Endpoint) return ret0 } @@ -275,10 +275,10 @@ func (mr *MockConfigurationMockRecorder) IssuerFromRequest(arg0 interface{}) *go } // KeysEndpoint mocks base method. -func (m *MockConfiguration) KeysEndpoint() op.Endpoint { +func (m *MockConfiguration) KeysEndpoint() *op.Endpoint { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "KeysEndpoint") - ret0, _ := ret[0].(op.Endpoint) + ret0, _ := ret[0].(*op.Endpoint) return ret0 } @@ -331,10 +331,10 @@ func (mr *MockConfigurationMockRecorder) RevocationAuthMethodPrivateKeyJWTSuppor } // RevocationEndpoint mocks base method. -func (m *MockConfiguration) RevocationEndpoint() op.Endpoint { +func (m *MockConfiguration) RevocationEndpoint() *op.Endpoint { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RevocationEndpoint") - ret0, _ := ret[0].(op.Endpoint) + ret0, _ := ret[0].(*op.Endpoint) return ret0 } @@ -373,10 +373,10 @@ func (mr *MockConfigurationMockRecorder) SupportedUILocales() *gomock.Call { } // TokenEndpoint mocks base method. -func (m *MockConfiguration) TokenEndpoint() op.Endpoint { +func (m *MockConfiguration) TokenEndpoint() *op.Endpoint { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "TokenEndpoint") - ret0, _ := ret[0].(op.Endpoint) + ret0, _ := ret[0].(*op.Endpoint) return ret0 } @@ -401,10 +401,10 @@ func (mr *MockConfigurationMockRecorder) TokenEndpointSigningAlgorithmsSupported } // UserinfoEndpoint mocks base method. -func (m *MockConfiguration) UserinfoEndpoint() op.Endpoint { +func (m *MockConfiguration) UserinfoEndpoint() *op.Endpoint { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UserinfoEndpoint") - ret0, _ := ret[0].(op.Endpoint) + ret0, _ := ret[0].(*op.Endpoint) return ret0 } diff --git a/pkg/op/op.go b/pkg/op/op.go index 0175d7f..55ee986 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -32,7 +32,7 @@ const ( ) var ( - DefaultEndpoints = &endpoints{ + DefaultEndpoints = &Endpoints{ Authorization: NewEndpoint(defaultAuthorizationEndpoint), Token: NewEndpoint(defaultTokenEndpoint), Introspection: NewEndpoint(defaultIntrospectEndpoint), @@ -131,16 +131,17 @@ type Config struct { DeviceAuthorization DeviceAuthorizationConfig } -type endpoints struct { - Authorization Endpoint - Token Endpoint - Introspection Endpoint - Userinfo Endpoint - Revocation Endpoint - EndSession Endpoint - CheckSessionIframe Endpoint - JwksURI Endpoint - DeviceAuthorization Endpoint +// Endpoints defines endpoint routes. +type Endpoints struct { + Authorization *Endpoint + Token *Endpoint + Introspection *Endpoint + Userinfo *Endpoint + Revocation *Endpoint + EndSession *Endpoint + CheckSessionIframe *Endpoint + JwksURI *Endpoint + DeviceAuthorization *Endpoint } // NewOpenIDProvider creates a provider. The provider provides (with HttpHandler()) @@ -212,7 +213,7 @@ type Provider struct { config *Config issuer IssuerFromRequest insecure bool - endpoints *endpoints + endpoints *Endpoints storage Storage keySet *openIDKeySet crypto Crypto @@ -233,35 +234,35 @@ func (o *Provider) Insecure() bool { return o.insecure } -func (o *Provider) AuthorizationEndpoint() Endpoint { +func (o *Provider) AuthorizationEndpoint() *Endpoint { return o.endpoints.Authorization } -func (o *Provider) TokenEndpoint() Endpoint { +func (o *Provider) TokenEndpoint() *Endpoint { return o.endpoints.Token } -func (o *Provider) IntrospectionEndpoint() Endpoint { +func (o *Provider) IntrospectionEndpoint() *Endpoint { return o.endpoints.Introspection } -func (o *Provider) UserinfoEndpoint() Endpoint { +func (o *Provider) UserinfoEndpoint() *Endpoint { return o.endpoints.Userinfo } -func (o *Provider) RevocationEndpoint() Endpoint { +func (o *Provider) RevocationEndpoint() *Endpoint { return o.endpoints.Revocation } -func (o *Provider) EndSessionEndpoint() Endpoint { +func (o *Provider) EndSessionEndpoint() *Endpoint { return o.endpoints.EndSession } -func (o *Provider) DeviceAuthorizationEndpoint() Endpoint { +func (o *Provider) DeviceAuthorizationEndpoint() *Endpoint { return o.endpoints.DeviceAuthorization } -func (o *Provider) KeysEndpoint() Endpoint { +func (o *Provider) KeysEndpoint() *Endpoint { return o.endpoints.JwksURI } @@ -420,7 +421,7 @@ func WithAllowInsecure() Option { } } -func WithCustomAuthEndpoint(endpoint Endpoint) Option { +func WithCustomAuthEndpoint(endpoint *Endpoint) Option { return func(o *Provider) error { if err := endpoint.Validate(); err != nil { return err @@ -430,7 +431,7 @@ func WithCustomAuthEndpoint(endpoint Endpoint) Option { } } -func WithCustomTokenEndpoint(endpoint Endpoint) Option { +func WithCustomTokenEndpoint(endpoint *Endpoint) Option { return func(o *Provider) error { if err := endpoint.Validate(); err != nil { return err @@ -440,7 +441,7 @@ func WithCustomTokenEndpoint(endpoint Endpoint) Option { } } -func WithCustomIntrospectionEndpoint(endpoint Endpoint) Option { +func WithCustomIntrospectionEndpoint(endpoint *Endpoint) Option { return func(o *Provider) error { if err := endpoint.Validate(); err != nil { return err @@ -450,7 +451,7 @@ func WithCustomIntrospectionEndpoint(endpoint Endpoint) Option { } } -func WithCustomUserinfoEndpoint(endpoint Endpoint) Option { +func WithCustomUserinfoEndpoint(endpoint *Endpoint) Option { return func(o *Provider) error { if err := endpoint.Validate(); err != nil { return err @@ -460,7 +461,7 @@ func WithCustomUserinfoEndpoint(endpoint Endpoint) Option { } } -func WithCustomRevocationEndpoint(endpoint Endpoint) Option { +func WithCustomRevocationEndpoint(endpoint *Endpoint) Option { return func(o *Provider) error { if err := endpoint.Validate(); err != nil { return err @@ -470,7 +471,7 @@ func WithCustomRevocationEndpoint(endpoint Endpoint) Option { } } -func WithCustomEndSessionEndpoint(endpoint Endpoint) Option { +func WithCustomEndSessionEndpoint(endpoint *Endpoint) Option { return func(o *Provider) error { if err := endpoint.Validate(); err != nil { return err @@ -480,7 +481,7 @@ func WithCustomEndSessionEndpoint(endpoint Endpoint) Option { } } -func WithCustomKeysEndpoint(endpoint Endpoint) Option { +func WithCustomKeysEndpoint(endpoint *Endpoint) Option { return func(o *Provider) error { if err := endpoint.Validate(); err != nil { return err @@ -490,7 +491,7 @@ func WithCustomKeysEndpoint(endpoint Endpoint) Option { } } -func WithCustomDeviceAuthorizationEndpoint(endpoint Endpoint) Option { +func WithCustomDeviceAuthorizationEndpoint(endpoint *Endpoint) Option { return func(o *Provider) error { if err := endpoint.Validate(); err != nil { return err @@ -500,8 +501,16 @@ func WithCustomDeviceAuthorizationEndpoint(endpoint Endpoint) Option { } } -func WithCustomEndpoints(auth, token, userInfo, revocation, endSession, keys Endpoint) Option { +// WithCustomEndpoints sets multiple endpoints at once. +// Non of the endpoints may be nil, or an error will +// be returned when the Option used by the Provider. +func WithCustomEndpoints(auth, token, userInfo, revocation, endSession, keys *Endpoint) Option { return func(o *Provider) error { + for _, e := range []*Endpoint{auth, token, userInfo, revocation, endSession, keys} { + if err := e.Validate(); err != nil { + return err + } + } o.endpoints.Authorization = auth o.endpoints.Token = token o.endpoints.Userinfo = userInfo diff --git a/pkg/op/op_test.go b/pkg/op/op_test.go index d33b39d..abe53bc 100644 --- a/pkg/op/op_test.go +++ b/pkg/op/op_test.go @@ -395,3 +395,54 @@ func TestRoutes(t *testing.T) { }) } } + +func TestWithCustomEndpoints(t *testing.T) { + type args struct { + auth *op.Endpoint + token *op.Endpoint + userInfo *op.Endpoint + revocation *op.Endpoint + endSession *op.Endpoint + keys *op.Endpoint + } + tests := []struct { + name string + args args + wantErr error + }{ + { + name: "all nil", + args: args{}, + wantErr: op.ErrNilEndpoint, + }, + { + name: "all set", + args: args{ + auth: op.NewEndpoint("/authorize"), + token: op.NewEndpoint("/oauth/token"), + userInfo: op.NewEndpoint("/userinfo"), + revocation: op.NewEndpoint("/revoke"), + endSession: op.NewEndpoint("/end_session"), + keys: op.NewEndpoint("/keys"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + provider, err := op.NewOpenIDProvider(testIssuer, testConfig, + storage.NewStorage(storage.NewUserStore(testIssuer)), + op.WithCustomEndpoints(tt.args.auth, tt.args.token, tt.args.userInfo, tt.args.revocation, tt.args.endSession, tt.args.keys), + ) + require.ErrorIs(t, err, tt.wantErr) + if tt.wantErr != nil { + return + } + assert.Equal(t, tt.args.auth, provider.AuthorizationEndpoint()) + assert.Equal(t, tt.args.token, provider.TokenEndpoint()) + assert.Equal(t, tt.args.userInfo, provider.UserinfoEndpoint()) + assert.Equal(t, tt.args.revocation, provider.RevocationEndpoint()) + assert.Equal(t, tt.args.endSession, provider.EndSessionEndpoint()) + assert.Equal(t, tt.args.keys, provider.KeysEndpoint()) + }) + } +} diff --git a/pkg/op/probes.go b/pkg/op/probes.go index 9ef5bb5..cb3853d 100644 --- a/pkg/op/probes.go +++ b/pkg/op/probes.go @@ -41,9 +41,9 @@ func ReadyStorage(s Storage) ProbesFn { } func ok(w http.ResponseWriter) { - httphelper.MarshalJSON(w, status{"ok"}) + httphelper.MarshalJSON(w, Status{"ok"}) } -type status struct { +type Status struct { Status string `json:"status,omitempty"` } diff --git a/pkg/op/server.go b/pkg/op/server.go new file mode 100644 index 0000000..a9cdcf5 --- /dev/null +++ b/pkg/op/server.go @@ -0,0 +1,346 @@ +package op + +import ( + "context" + "net/http" + "net/url" + + "github.com/muhlemmer/gu" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" +) + +// Server describes the interface that needs to be implemented to serve +// OpenID Connect and Oauth2 standard requests. +// +// Methods are called after the HTTP route is resolved and +// the request body is parsed into the Request's Data field. +// When a method is called, it can be assumed that required fields, +// as described in their relevant standard, are validated already. +// The Response Data field may be of any type to allow flexibility +// to extend responses with custom fields. There are however requirements +// in the standards regarding the response models. Where applicable +// the method documentation gives a recommended type which can be used +// directly or extended upon. +// +// The addition of new methods is not considered a breaking change +// as defined by semver rules. +// Implementations MUST embed [UnimplementedServer] to maintain +// forward compatibility. +// +// EXPERIMENTAL: may change until v4 +type Server interface { + // Health returns a status of "ok" once the Server is listening. + // The recommended Response Data type is [Status]. + Health(context.Context, *Request[struct{}]) (*Response, error) + + // Ready returns a status of "ok" once all dependencies, + // such as database storage, are ready. + // An error can be returned to explain what is not ready. + // The recommended Response Data type is [Status]. + Ready(context.Context, *Request[struct{}]) (*Response, error) + + // Discovery returns the OpenID Provider Configuration Information for this server. + // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig + // The recommended Response Data type is [oidc.DiscoveryConfiguration]. + Discovery(context.Context, *Request[struct{}]) (*Response, error) + + // Keys serves the JWK set which the client can use verify signatures from the op. + // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata `jwks_uri` key. + // The recommended Response Data type is [jose.JSONWebKeySet]. + Keys(context.Context, *Request[struct{}]) (*Response, error) + + // VerifyAuthRequest verifies the Auth Request and + // adds the Client to the request. + // + // When the `request` field is populated with a + // "Request Object" JWT, it needs to be Validated + // and its claims overwrite any fields in the AuthRequest. + // If the implementation does not support "Request Object", + // it MUST return an [oidc.ErrRequestNotSupported]. + // https://openid.net/specs/openid-connect-core-1_0.html#RequestObject + VerifyAuthRequest(context.Context, *Request[oidc.AuthRequest]) (*ClientRequest[oidc.AuthRequest], error) + + // Authorize initiates the authorization flow and redirects to a login page. + // See the various https://openid.net/specs/openid-connect-core-1_0.html + // authorize endpoint sections (one for each type of flow). + Authorize(context.Context, *ClientRequest[oidc.AuthRequest]) (*Redirect, error) + + // DeviceAuthorization initiates the device authorization flow. + // https://datatracker.ietf.org/doc/html/rfc8628#section-3.1 + // The recommended Response Data type is [oidc.DeviceAuthorizationResponse]. + DeviceAuthorization(context.Context, *ClientRequest[oidc.DeviceAuthorizationRequest]) (*Response, error) + + // VerifyClient is called on most oauth/token handlers to authenticate, + // using either a secret (POST, Basic) or assertion (JWT). + // If no secrets are provided, the client must be public. + // This method is called before each method that takes a + // [ClientRequest] argument. + VerifyClient(context.Context, *Request[ClientCredentials]) (Client, error) + + // CodeExchange returns Tokens after an authorization code + // is obtained in a successful Authorize flow. + // It is called by the Token endpoint handler when + // grant_type has the value authorization_code + // https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint + // The recommended Response Data type is [oidc.AccessTokenResponse]. + CodeExchange(context.Context, *ClientRequest[oidc.AccessTokenRequest]) (*Response, error) + + // RefreshToken returns new Tokens after verifying a Refresh token. + // It is called by the Token endpoint handler when + // grant_type has the value refresh_token + // https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens + // The recommended Response Data type is [oidc.AccessTokenResponse]. + RefreshToken(context.Context, *ClientRequest[oidc.RefreshTokenRequest]) (*Response, error) + + // JWTProfile handles the OAuth 2.0 JWT Profile Authorization Grant + // It is called by the Token endpoint handler when + // grant_type has the value urn:ietf:params:oauth:grant-type:jwt-bearer + // https://datatracker.ietf.org/doc/html/rfc7523#section-2.1 + // The recommended Response Data type is [oidc.AccessTokenResponse]. + JWTProfile(context.Context, *Request[oidc.JWTProfileGrantRequest]) (*Response, error) + + // TokenExchange handles the OAuth 2.0 token exchange grant + // It is called by the Token endpoint handler when + // grant_type has the value urn:ietf:params:oauth:grant-type:token-exchange + // https://datatracker.ietf.org/doc/html/rfc8693 + // The recommended Response Data type is [oidc.AccessTokenResponse]. + TokenExchange(context.Context, *ClientRequest[oidc.TokenExchangeRequest]) (*Response, error) + + // ClientCredentialsExchange handles the OAuth 2.0 client credentials grant + // It is called by the Token endpoint handler when + // grant_type has the value client_credentials + // https://datatracker.ietf.org/doc/html/rfc6749#section-4.4 + // The recommended Response Data type is [oidc.AccessTokenResponse]. + ClientCredentialsExchange(context.Context, *ClientRequest[oidc.ClientCredentialsRequest]) (*Response, error) + + // DeviceToken handles the OAuth 2.0 Device Authorization Grant + // It is called by the Token endpoint handler when + // grant_type has the value urn:ietf:params:oauth:grant-type:device_code. + // It is typically called in a polling fashion and appropriate errors + // should be returned to signal authorization_pending or access_denied etc. + // https://datatracker.ietf.org/doc/html/rfc8628#section-3.4, + // https://datatracker.ietf.org/doc/html/rfc8628#section-3.5. + // The recommended Response Data type is [oidc.AccessTokenResponse]. + DeviceToken(context.Context, *ClientRequest[oidc.DeviceAccessTokenRequest]) (*Response, error) + + // Introspect handles the OAuth 2.0 Token Introspection endpoint. + // https://datatracker.ietf.org/doc/html/rfc7662 + // The recommended Response Data type is [oidc.IntrospectionResponse]. + Introspect(context.Context, *ClientRequest[oidc.IntrospectionRequest]) (*Response, error) + + // UserInfo handles the UserInfo endpoint and returns Claims about the authenticated End-User. + // https://openid.net/specs/openid-connect-core-1_0.html#UserInfo + // The recommended Response Data type is [oidc.UserInfo]. + UserInfo(context.Context, *Request[oidc.UserInfoRequest]) (*Response, error) + + // Revocation handles token revocation using an access or refresh token. + // https://datatracker.ietf.org/doc/html/rfc7009 + // There are no response requirements. Data may remain empty. + Revocation(context.Context, *ClientRequest[oidc.RevocationRequest]) (*Response, error) + + // EndSession handles the OpenID Connect RP-Initiated Logout. + // https://openid.net/specs/openid-connect-rpinitiated-1_0.html + // There are no response requirements. Data may remain empty. + EndSession(context.Context, *Request[oidc.EndSessionRequest]) (*Redirect, error) + + // mustImpl forces implementations to embed the UnimplementedServer for forward + // compatibility with the interface. + mustImpl() +} + +// Request contains the [http.Request] informational fields +// and parsed Data from the request body (POST) or URL parameters (GET). +// Data can be assumed to be validated according to the applicable +// standard for the specific endpoints. +// +// EXPERIMENTAL: may change until v4 +type Request[T any] struct { + Method string + URL *url.URL + Header http.Header + Form url.Values + PostForm url.Values + Data *T +} + +func (r *Request[_]) path() string { + return r.URL.Path +} + +func newRequest[T any](r *http.Request, data *T) *Request[T] { + return &Request[T]{ + Method: r.Method, + URL: r.URL, + Header: r.Header, + Form: r.Form, + PostForm: r.PostForm, + Data: data, + } +} + +// ClientRequest is a Request with a verified client attached to it. +// Methods that receive this argument may assume the client was authenticated, +// or verified to be a public client. +// +// EXPERIMENTAL: may change until v4 +type ClientRequest[T any] struct { + *Request[T] + Client Client +} + +func newClientRequest[T any](r *http.Request, data *T, client Client) *ClientRequest[T] { + return &ClientRequest[T]{ + Request: newRequest[T](r, data), + Client: client, + } +} + +// Response object for most [Server] methods. +// +// EXPERIMENTAL: may change until v4 +type Response struct { + // Header map will be merged with the + // header on the [http.ResponseWriter]. + Header http.Header + + // Data will be JSON marshaled to + // the response body. + // We allow any type, so that implementations + // can extend the standard types as they wish. + // However, each method will recommend which + // (base) type to use as model, in order to + // be compliant with the standards. + Data any +} + +// NewResponse creates a new response for data, +// without custom headers. +func NewResponse(data any) *Response { + return &Response{ + Data: data, + } +} + +func (resp *Response) writeOut(w http.ResponseWriter) { + gu.MapMerge(resp.Header, w.Header()) + httphelper.MarshalJSON(w, resp.Data) +} + +// Redirect is a special response type which will +// initiate a [http.StatusFound] redirect. +// The Params field will be encoded and set to the +// URL's RawQuery field before building the URL. +// +// EXPERIMENTAL: may change until v4 +type Redirect struct { + // Header map will be merged with the + // header on the [http.ResponseWriter]. + Header http.Header + + URL string +} + +func NewRedirect(url string) *Redirect { + return &Redirect{URL: url} +} + +func (red *Redirect) writeOut(w http.ResponseWriter, r *http.Request) { + gu.MapMerge(r.Header, w.Header()) + http.Redirect(w, r, red.URL, http.StatusFound) +} + +type UnimplementedServer struct{} + +// UnimplementedStatusCode is the status code returned for methods +// that are not yet implemented. +// Note that this means methods in the sense of the Go interface, +// and not http methods covered by "501 Not Implemented". +var UnimplementedStatusCode = http.StatusNotFound + +func unimplementedError(r interface{ path() string }) StatusError { + err := oidc.ErrServerError().WithDescription("%s not implemented on this server", r.path()) + return NewStatusError(err, UnimplementedStatusCode) +} + +func unimplementedGrantError(gt oidc.GrantType) StatusError { + err := oidc.ErrUnsupportedGrantType().WithDescription("%s not supported", gt) + return NewStatusError(err, http.StatusBadRequest) // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 +} + +func (UnimplementedServer) mustImpl() {} + +func (UnimplementedServer) Health(ctx context.Context, r *Request[struct{}]) (*Response, error) { + return nil, unimplementedError(r) +} + +func (UnimplementedServer) Ready(ctx context.Context, r *Request[struct{}]) (*Response, error) { + return nil, unimplementedError(r) +} + +func (UnimplementedServer) Discovery(ctx context.Context, r *Request[struct{}]) (*Response, error) { + return nil, unimplementedError(r) +} + +func (UnimplementedServer) Keys(ctx context.Context, r *Request[struct{}]) (*Response, error) { + return nil, unimplementedError(r) +} + +func (UnimplementedServer) VerifyAuthRequest(ctx context.Context, r *Request[oidc.AuthRequest]) (*ClientRequest[oidc.AuthRequest], error) { + if r.Data.RequestParam != "" { + return nil, oidc.ErrRequestNotSupported() + } + return nil, unimplementedError(r) +} + +func (UnimplementedServer) Authorize(ctx context.Context, r *ClientRequest[oidc.AuthRequest]) (*Redirect, error) { + return nil, unimplementedError(r) +} + +func (UnimplementedServer) DeviceAuthorization(ctx context.Context, r *ClientRequest[oidc.DeviceAuthorizationRequest]) (*Response, error) { + return nil, unimplementedError(r) +} + +func (UnimplementedServer) VerifyClient(ctx context.Context, r *Request[ClientCredentials]) (Client, error) { + return nil, unimplementedError(r) +} + +func (UnimplementedServer) CodeExchange(ctx context.Context, r *ClientRequest[oidc.AccessTokenRequest]) (*Response, error) { + return nil, unimplementedGrantError(oidc.GrantTypeCode) +} + +func (UnimplementedServer) RefreshToken(ctx context.Context, r *ClientRequest[oidc.RefreshTokenRequest]) (*Response, error) { + return nil, unimplementedGrantError(oidc.GrantTypeRefreshToken) +} + +func (UnimplementedServer) JWTProfile(ctx context.Context, r *Request[oidc.JWTProfileGrantRequest]) (*Response, error) { + return nil, unimplementedGrantError(oidc.GrantTypeBearer) +} + +func (UnimplementedServer) TokenExchange(ctx context.Context, r *ClientRequest[oidc.TokenExchangeRequest]) (*Response, error) { + return nil, unimplementedGrantError(oidc.GrantTypeTokenExchange) +} + +func (UnimplementedServer) ClientCredentialsExchange(ctx context.Context, r *ClientRequest[oidc.ClientCredentialsRequest]) (*Response, error) { + return nil, unimplementedGrantError(oidc.GrantTypeClientCredentials) +} + +func (UnimplementedServer) DeviceToken(ctx context.Context, r *ClientRequest[oidc.DeviceAccessTokenRequest]) (*Response, error) { + return nil, unimplementedGrantError(oidc.GrantTypeDeviceCode) +} + +func (UnimplementedServer) Introspect(ctx context.Context, r *ClientRequest[oidc.IntrospectionRequest]) (*Response, error) { + return nil, unimplementedError(r) +} + +func (UnimplementedServer) UserInfo(ctx context.Context, r *Request[oidc.UserInfoRequest]) (*Response, error) { + return nil, unimplementedError(r) +} + +func (UnimplementedServer) Revocation(ctx context.Context, r *ClientRequest[oidc.RevocationRequest]) (*Response, error) { + return nil, unimplementedError(r) +} + +func (UnimplementedServer) EndSession(ctx context.Context, r *Request[oidc.EndSessionRequest]) (*Redirect, error) { + return nil, unimplementedError(r) +} diff --git a/pkg/op/server_http.go b/pkg/op/server_http.go new file mode 100644 index 0000000..3fb481d --- /dev/null +++ b/pkg/op/server_http.go @@ -0,0 +1,480 @@ +package op + +import ( + "context" + "net/http" + "net/url" + + "github.com/go-chi/chi" + "github.com/rs/cors" + "github.com/zitadel/logging" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/schema" + "golang.org/x/exp/slog" +) + +// RegisterServer registers an implementation of Server. +// The resulting handler takes care of routing and request parsing, +// with some basic validation of required fields. +// The routes can be customized with [WithEndpoints]. +// +// EXPERIMENTAL: may change until v4 +func RegisterServer(server Server, endpoints Endpoints, options ...ServerOption) http.Handler { + decoder := schema.NewDecoder() + decoder.IgnoreUnknownKeys(true) + + ws := &webServer{ + server: server, + endpoints: endpoints, + decoder: decoder, + logger: slog.Default(), + } + + for _, option := range options { + option(ws) + } + + ws.createRouter() + return ws +} + +type ServerOption func(s *webServer) + +// WithHTTPMiddleware sets the passed middleware chain to the root of +// the Server's router. +func WithHTTPMiddleware(m ...func(http.Handler) http.Handler) ServerOption { + return func(s *webServer) { + s.middleware = m + } +} + +// WithDecoder overrides the default decoder, +// which is a [schema.Decoder] with IgnoreUnknownKeys set to true. +func WithDecoder(decoder httphelper.Decoder) ServerOption { + return func(s *webServer) { + s.decoder = decoder + } +} + +// WithFallbackLogger overrides the fallback logger, which +// is used when no logger was found in the context. +// Defaults to [slog.Default]. +func WithFallbackLogger(logger *slog.Logger) ServerOption { + return func(s *webServer) { + s.logger = logger + } +} + +type webServer struct { + http.Handler + server Server + middleware []func(http.Handler) http.Handler + endpoints Endpoints + decoder httphelper.Decoder + logger *slog.Logger +} + +func (s *webServer) getLogger(ctx context.Context) *slog.Logger { + if logger, ok := logging.FromContext(ctx); ok { + return logger + } + return s.logger +} + +func (s *webServer) createRouter() { + router := chi.NewRouter() + router.Use(cors.New(defaultCORSOptions).Handler) + router.Use(s.middleware...) + router.HandleFunc(healthEndpoint, simpleHandler(s, s.server.Health)) + router.HandleFunc(readinessEndpoint, simpleHandler(s, s.server.Ready)) + router.HandleFunc(oidc.DiscoveryEndpoint, simpleHandler(s, s.server.Discovery)) + + s.endpointRoute(router, s.endpoints.Authorization, s.authorizeHandler) + s.endpointRoute(router, s.endpoints.DeviceAuthorization, s.withClient(s.deviceAuthorizationHandler)) + s.endpointRoute(router, s.endpoints.Token, s.tokensHandler) + s.endpointRoute(router, s.endpoints.Introspection, s.withClient(s.introspectionHandler)) + s.endpointRoute(router, s.endpoints.Userinfo, s.userInfoHandler) + s.endpointRoute(router, s.endpoints.Revocation, s.withClient(s.revocationHandler)) + s.endpointRoute(router, s.endpoints.EndSession, s.endSessionHandler) + s.endpointRoute(router, s.endpoints.JwksURI, simpleHandler(s, s.server.Keys)) + s.Handler = router +} + +func (s *webServer) endpointRoute(router *chi.Mux, e *Endpoint, hf http.HandlerFunc) { + if e != nil { + router.HandleFunc(e.Relative(), hf) + s.logger.Info("registered route", "endpoint", e.Relative()) + } +} + +type clientHandler func(w http.ResponseWriter, r *http.Request, client Client) + +func (s *webServer) withClient(handler clientHandler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + client, err := s.verifyRequestClient(r) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + if grantType := oidc.GrantType(r.Form.Get("grant_type")); grantType != "" { + if !ValidateGrantType(client, grantType) { + WriteError(w, r, oidc.ErrUnauthorizedClient().WithDescription("grant_type %q not allowed", grantType), s.getLogger(r.Context())) + return + } + } + handler(w, r, client) + } +} + +func (s *webServer) verifyRequestClient(r *http.Request) (_ Client, err error) { + if err = r.ParseForm(); err != nil { + return nil, oidc.ErrInvalidRequest().WithDescription("error parsing form").WithParent(err) + } + cc := new(ClientCredentials) + if err = s.decoder.Decode(cc, r.Form); err != nil { + return nil, oidc.ErrInvalidRequest().WithDescription("error decoding form").WithParent(err) + } + // Basic auth takes precedence, so if set it overwrites the form data. + if clientID, clientSecret, ok := r.BasicAuth(); ok { + cc.ClientID, err = url.QueryUnescape(clientID) + if err != nil { + return nil, oidc.ErrInvalidClient().WithDescription("invalid basic auth header").WithParent(err) + } + cc.ClientSecret, err = url.QueryUnescape(clientSecret) + if err != nil { + return nil, oidc.ErrInvalidClient().WithDescription("invalid basic auth header").WithParent(err) + } + } + if cc.ClientID == "" && cc.ClientAssertion == "" { + return nil, oidc.ErrInvalidRequest().WithDescription("client_id or client_assertion must be provided") + } + if cc.ClientAssertion != "" && cc.ClientAssertionType != oidc.ClientAssertionTypeJWTAssertion { + return nil, oidc.ErrInvalidRequest().WithDescription("invalid client_assertion_type %s", cc.ClientAssertionType) + } + return s.server.VerifyClient(r.Context(), &Request[ClientCredentials]{ + Method: r.Method, + URL: r.URL, + Header: r.Header, + Form: r.Form, + Data: cc, + }) +} + +func (s *webServer) authorizeHandler(w http.ResponseWriter, r *http.Request) { + request, err := decodeRequest[oidc.AuthRequest](s.decoder, r, false) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + redirect, err := s.authorize(r.Context(), newRequest(r, request)) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + redirect.writeOut(w, r) +} + +func (s *webServer) authorize(ctx context.Context, r *Request[oidc.AuthRequest]) (_ *Redirect, err error) { + cr, err := s.server.VerifyAuthRequest(ctx, r) + if err != nil { + return nil, err + } + authReq := cr.Data + if authReq.RedirectURI == "" { + return nil, ErrAuthReqMissingRedirectURI + } + authReq.MaxAge, err = ValidateAuthReqPrompt(authReq.Prompt, authReq.MaxAge) + if err != nil { + return nil, err + } + authReq.Scopes, err = ValidateAuthReqScopes(cr.Client, authReq.Scopes) + if err != nil { + return nil, err + } + if err := ValidateAuthReqRedirectURI(cr.Client, authReq.RedirectURI, authReq.ResponseType); err != nil { + return nil, err + } + if err := ValidateAuthReqResponseType(cr.Client, authReq.ResponseType); err != nil { + return nil, err + } + return s.server.Authorize(ctx, cr) +} + +func (s *webServer) deviceAuthorizationHandler(w http.ResponseWriter, r *http.Request, client Client) { + request, err := decodeRequest[oidc.DeviceAuthorizationRequest](s.decoder, r, false) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + resp, err := s.server.DeviceAuthorization(r.Context(), newClientRequest(r, request, client)) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + resp.writeOut(w) +} + +func (s *webServer) tokensHandler(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + WriteError(w, r, oidc.ErrInvalidRequest().WithDescription("error parsing form").WithParent(err), s.getLogger(r.Context())) + return + } + + switch grantType := oidc.GrantType(r.Form.Get("grant_type")); grantType { + case oidc.GrantTypeCode: + s.withClient(s.codeExchangeHandler)(w, r) + case oidc.GrantTypeRefreshToken: + s.withClient(s.refreshTokenHandler)(w, r) + case oidc.GrantTypeClientCredentials: + s.withClient(s.clientCredentialsHandler)(w, r) + case oidc.GrantTypeBearer: + s.jwtProfileHandler(w, r) + case oidc.GrantTypeTokenExchange: + s.withClient(s.tokenExchangeHandler)(w, r) + case oidc.GrantTypeDeviceCode: + s.withClient(s.deviceTokenHandler)(w, r) + case "": + WriteError(w, r, oidc.ErrInvalidRequest().WithDescription("grant_type missing"), s.getLogger(r.Context())) + default: + WriteError(w, r, unimplementedGrantError(grantType), s.getLogger(r.Context())) + } +} + +func (s *webServer) jwtProfileHandler(w http.ResponseWriter, r *http.Request) { + request, err := decodeRequest[oidc.JWTProfileGrantRequest](s.decoder, r, false) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + if request.Assertion == "" { + WriteError(w, r, oidc.ErrInvalidRequest().WithDescription("assertion missing"), s.getLogger(r.Context())) + return + } + resp, err := s.server.JWTProfile(r.Context(), newRequest(r, request)) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + resp.writeOut(w) +} + +func (s *webServer) codeExchangeHandler(w http.ResponseWriter, r *http.Request, client Client) { + request, err := decodeRequest[oidc.AccessTokenRequest](s.decoder, r, false) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + if request.Code == "" { + WriteError(w, r, oidc.ErrInvalidRequest().WithDescription("code missing"), s.getLogger(r.Context())) + return + } + if request.RedirectURI == "" { + WriteError(w, r, oidc.ErrInvalidRequest().WithDescription("redirect_uri missing"), s.getLogger(r.Context())) + return + } + resp, err := s.server.CodeExchange(r.Context(), newClientRequest(r, request, client)) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + resp.writeOut(w) +} + +func (s *webServer) refreshTokenHandler(w http.ResponseWriter, r *http.Request, client Client) { + request, err := decodeRequest[oidc.RefreshTokenRequest](s.decoder, r, false) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + if request.RefreshToken == "" { + WriteError(w, r, oidc.ErrInvalidRequest().WithDescription("refresh_token missing"), s.getLogger(r.Context())) + return + } + resp, err := s.server.RefreshToken(r.Context(), newClientRequest(r, request, client)) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + resp.writeOut(w) +} + +func (s *webServer) tokenExchangeHandler(w http.ResponseWriter, r *http.Request, client Client) { + request, err := decodeRequest[oidc.TokenExchangeRequest](s.decoder, r, false) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + if request.SubjectToken == "" { + WriteError(w, r, oidc.ErrInvalidRequest().WithDescription("subject_token missing"), s.getLogger(r.Context())) + return + } + if request.SubjectTokenType == "" { + WriteError(w, r, oidc.ErrInvalidRequest().WithDescription("subject_token_type missing"), s.getLogger(r.Context())) + return + } + if !request.SubjectTokenType.IsSupported() { + WriteError(w, r, oidc.ErrInvalidRequest().WithDescription("subject_token_type is not supported"), s.getLogger(r.Context())) + return + } + if request.RequestedTokenType != "" && !request.RequestedTokenType.IsSupported() { + WriteError(w, r, oidc.ErrInvalidRequest().WithDescription("requested_token_type is not supported"), s.getLogger(r.Context())) + return + } + if request.ActorTokenType != "" && !request.ActorTokenType.IsSupported() { + WriteError(w, r, oidc.ErrInvalidRequest().WithDescription("actor_token_type is not supported"), s.getLogger(r.Context())) + return + } + resp, err := s.server.TokenExchange(r.Context(), newClientRequest(r, request, client)) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + resp.writeOut(w) +} + +func (s *webServer) clientCredentialsHandler(w http.ResponseWriter, r *http.Request, client Client) { + if client.AuthMethod() == oidc.AuthMethodNone { + WriteError(w, r, oidc.ErrInvalidClient().WithDescription("client must be authenticated"), s.getLogger(r.Context())) + return + } + + request, err := decodeRequest[oidc.ClientCredentialsRequest](s.decoder, r, false) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + resp, err := s.server.ClientCredentialsExchange(r.Context(), newClientRequest(r, request, client)) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + resp.writeOut(w) +} + +func (s *webServer) deviceTokenHandler(w http.ResponseWriter, r *http.Request, client Client) { + request, err := decodeRequest[oidc.DeviceAccessTokenRequest](s.decoder, r, false) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + if request.DeviceCode == "" { + WriteError(w, r, oidc.ErrInvalidRequest().WithDescription("device_code missing"), s.getLogger(r.Context())) + return + } + resp, err := s.server.DeviceToken(r.Context(), newClientRequest(r, request, client)) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + resp.writeOut(w) +} + +func (s *webServer) introspectionHandler(w http.ResponseWriter, r *http.Request, client Client) { + if client.AuthMethod() == oidc.AuthMethodNone { + WriteError(w, r, oidc.ErrInvalidClient().WithDescription("client must be authenticated"), s.getLogger(r.Context())) + return + } + request, err := decodeRequest[oidc.IntrospectionRequest](s.decoder, r, false) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + if request.Token == "" { + WriteError(w, r, oidc.ErrInvalidRequest().WithDescription("token missing"), s.getLogger(r.Context())) + return + } + resp, err := s.server.Introspect(r.Context(), newClientRequest(r, request, client)) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + resp.writeOut(w) +} + +func (s *webServer) userInfoHandler(w http.ResponseWriter, r *http.Request) { + request, err := decodeRequest[oidc.UserInfoRequest](s.decoder, r, false) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + if token, err := getAccessToken(r); err == nil { + request.AccessToken = token + } + if request.AccessToken == "" { + err = NewStatusError( + oidc.ErrInvalidRequest().WithDescription("access token missing"), + http.StatusUnauthorized, + ) + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + resp, err := s.server.UserInfo(r.Context(), newRequest(r, request)) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + resp.writeOut(w) +} + +func (s *webServer) revocationHandler(w http.ResponseWriter, r *http.Request, client Client) { + request, err := decodeRequest[oidc.RevocationRequest](s.decoder, r, false) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + if request.Token == "" { + WriteError(w, r, oidc.ErrInvalidRequest().WithDescription("token missing"), s.getLogger(r.Context())) + return + } + resp, err := s.server.Revocation(r.Context(), newClientRequest(r, request, client)) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + resp.writeOut(w) +} + +func (s *webServer) endSessionHandler(w http.ResponseWriter, r *http.Request) { + request, err := decodeRequest[oidc.EndSessionRequest](s.decoder, r, false) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + resp, err := s.server.EndSession(r.Context(), newRequest(r, request)) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + resp.writeOut(w, r) +} + +func simpleHandler(s *webServer, method func(context.Context, *Request[struct{}]) (*Response, error)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + WriteError(w, r, oidc.ErrInvalidRequest().WithDescription("error parsing form").WithParent(err), s.getLogger(r.Context())) + return + } + resp, err := method(r.Context(), newRequest(r, &struct{}{})) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + resp.writeOut(w) + } +} + +func decodeRequest[R any](decoder httphelper.Decoder, r *http.Request, postOnly bool) (*R, error) { + dst := new(R) + if err := r.ParseForm(); err != nil { + return nil, oidc.ErrInvalidRequest().WithDescription("error parsing form").WithParent(err) + } + form := r.Form + if postOnly { + form = r.PostForm + } + if err := decoder.Decode(dst, form); err != nil { + return nil, oidc.ErrInvalidRequest().WithDescription("error decoding form").WithParent(err) + } + return dst, nil +} diff --git a/pkg/op/server_http_routes_test.go b/pkg/op/server_http_routes_test.go new file mode 100644 index 0000000..6a8b75d --- /dev/null +++ b/pkg/op/server_http_routes_test.go @@ -0,0 +1,345 @@ +package op_test + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/oidc/v3/pkg/client" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" +) + +func jwtProfile() (string, error) { + keyData, err := client.ConfigFromKeyFile("../../example/server/service-key1.json") + if err != nil { + return "", err + } + signer, err := client.NewSignerFromPrivateKeyByte([]byte(keyData.Key), keyData.KeyID) + if err != nil { + return "", err + } + return client.SignedJWTProfileAssertion(keyData.UserID, []string{testIssuer}, time.Hour, signer) +} + +func TestServerRoutes(t *testing.T) { + server := op.NewLegacyServer(testProvider, *op.DefaultEndpoints) + + storage := testProvider.Storage().(routesTestStorage) + ctx := op.ContextWithIssuer(context.Background(), testIssuer) + + client, err := storage.GetClientByClientID(ctx, "web") + require.NoError(t, err) + + oidcAuthReq := &oidc.AuthRequest{ + ClientID: client.GetID(), + RedirectURI: "https://example.com", + MaxAge: gu.Ptr[uint](300), + Scopes: oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess, oidc.ScopeEmail, oidc.ScopeProfile, oidc.ScopePhone}, + ResponseType: oidc.ResponseTypeCode, + } + + authReq, err := storage.CreateAuthRequest(ctx, oidcAuthReq, "id1") + require.NoError(t, err) + storage.AuthRequestDone(authReq.GetID()) + + accessToken, refreshToken, _, err := op.CreateAccessToken(ctx, authReq, op.AccessTokenTypeBearer, testProvider, client, "") + require.NoError(t, err) + accessTokenRevoke, _, _, err := op.CreateAccessToken(ctx, authReq, op.AccessTokenTypeBearer, testProvider, client, "") + require.NoError(t, err) + idToken, err := op.CreateIDToken(ctx, testIssuer, authReq, time.Hour, accessToken, "123", storage, client) + require.NoError(t, err) + jwtToken, _, _, err := op.CreateAccessToken(ctx, authReq, op.AccessTokenTypeJWT, testProvider, client, "") + require.NoError(t, err) + jwtProfileToken, err := jwtProfile() + require.NoError(t, err) + + oidcAuthReq.IDTokenHint = idToken + + serverURL, err := url.Parse(testIssuer) + require.NoError(t, err) + + type basicAuth struct { + username, password string + } + + tests := []struct { + name string + method string + path string + basicAuth *basicAuth + header map[string]string + values map[string]string + body map[string]string + wantCode int + headerContains map[string]string + json string // test for exact json output + contains []string // when the body output is not constant, we just check for snippets to be present in the response + }{ + { + name: "health", + method: http.MethodGet, + path: "/healthz", + wantCode: http.StatusOK, + json: `{"status":"ok"}`, + }, + { + name: "ready", + method: http.MethodGet, + path: "/ready", + wantCode: http.StatusOK, + json: `{"status":"ok"}`, + }, + { + name: "discovery", + method: http.MethodGet, + path: oidc.DiscoveryEndpoint, + wantCode: http.StatusOK, + json: `{"issuer":"https://localhost:9998/","authorization_endpoint":"https://localhost:9998/authorize","token_endpoint":"https://localhost:9998/oauth/token","introspection_endpoint":"https://localhost:9998/oauth/introspect","userinfo_endpoint":"https://localhost:9998/userinfo","revocation_endpoint":"https://localhost:9998/revoke","end_session_endpoint":"https://localhost:9998/end_session","device_authorization_endpoint":"https://localhost:9998/device_authorization","jwks_uri":"https://localhost:9998/keys","scopes_supported":["openid","profile","email","phone","address","offline_access"],"response_types_supported":["code","id_token","id_token token"],"grant_types_supported":["authorization_code","implicit","refresh_token","client_credentials","urn:ietf:params:oauth:grant-type:token-exchange","urn:ietf:params:oauth:grant-type:jwt-bearer","urn:ietf:params:oauth:grant-type:device_code"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"],"request_object_signing_alg_values_supported":["RS256"],"token_endpoint_auth_methods_supported":["none","client_secret_basic","client_secret_post","private_key_jwt"],"token_endpoint_auth_signing_alg_values_supported":["RS256"],"revocation_endpoint_auth_methods_supported":["none","client_secret_basic","client_secret_post","private_key_jwt"],"revocation_endpoint_auth_signing_alg_values_supported":["RS256"],"introspection_endpoint_auth_methods_supported":["client_secret_basic","private_key_jwt"],"introspection_endpoint_auth_signing_alg_values_supported":["RS256"],"claims_supported":["sub","aud","exp","iat","iss","auth_time","nonce","acr","amr","c_hash","at_hash","act","scopes","client_id","azp","preferred_username","name","family_name","given_name","locale","email","email_verified","phone_number","phone_number_verified"],"code_challenge_methods_supported":["S256"],"ui_locales_supported":["en"],"request_parameter_supported":true,"request_uri_parameter_supported":false}`, + }, + { + name: "authorization", + method: http.MethodGet, + path: testProvider.AuthorizationEndpoint().Relative(), + values: map[string]string{ + "client_id": client.GetID(), + "redirect_uri": "https://example.com", + "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.String(), + "response_type": string(oidc.ResponseTypeCode), + }, + wantCode: http.StatusFound, + headerContains: map[string]string{"Location": "/login/username?authRequestID="}, + }, + { + // This call will fail. A successfull test is already + // part of client/integration_test.go + name: "code exchange", + method: http.MethodGet, + path: testProvider.TokenEndpoint().Relative(), + values: map[string]string{ + "grant_type": string(oidc.GrantTypeCode), + "client_id": client.GetID(), + "client_secret": "secret", + "redirect_uri": "https://example.com", + "code": "123", + }, + wantCode: http.StatusBadRequest, + json: `{"error":"invalid_grant", "error_description":"invalid code"}`, + }, + { + name: "JWT authorization", + method: http.MethodGet, + path: testProvider.TokenEndpoint().Relative(), + values: map[string]string{ + "grant_type": string(oidc.GrantTypeBearer), + "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.String(), + "assertion": jwtProfileToken, + }, + wantCode: http.StatusOK, + contains: []string{`{"access_token":`, `"token_type":"Bearer","expires_in":299}`}, + }, + { + name: "Token exchange", + method: http.MethodGet, + path: testProvider.TokenEndpoint().Relative(), + basicAuth: &basicAuth{"web", "secret"}, + values: map[string]string{ + "grant_type": string(oidc.GrantTypeTokenExchange), + "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.String(), + "subject_token": jwtToken, + "subject_token_type": string(oidc.AccessTokenType), + }, + wantCode: http.StatusOK, + contains: []string{ + `{"access_token":"`, + `","issued_token_type":"urn:ietf:params:oauth:token-type:refresh_token","token_type":"Bearer","expires_in":299,"scope":"openid offline_access","refresh_token":"`, + }, + }, + { + name: "Client credentials exchange", + method: http.MethodGet, + path: testProvider.TokenEndpoint().Relative(), + basicAuth: &basicAuth{"sid1", "verysecret"}, + values: map[string]string{ + "grant_type": string(oidc.GrantTypeClientCredentials), + "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.String(), + }, + wantCode: http.StatusOK, + contains: []string{`{"access_token":"`, `","token_type":"Bearer","expires_in":299}`}, + }, + { + // This call will fail. A successfull test is already + // part of device_test.go + name: "device token", + method: http.MethodPost, + path: testProvider.TokenEndpoint().Relative(), + basicAuth: &basicAuth{"web", "secret"}, + header: map[string]string{ + "Content-Type": "application/x-www-form-urlencoded", + }, + body: map[string]string{ + "grant_type": string(oidc.GrantTypeDeviceCode), + "device_code": "123", + }, + wantCode: http.StatusBadRequest, + json: `{"error":"access_denied","error_description":"The authorization request was denied."}`, + }, + { + name: "missing grant type", + method: http.MethodGet, + path: testProvider.TokenEndpoint().Relative(), + wantCode: http.StatusBadRequest, + json: `{"error":"invalid_request","error_description":"grant_type missing"}`, + }, + { + name: "unsupported grant type", + method: http.MethodGet, + path: testProvider.TokenEndpoint().Relative(), + values: map[string]string{ + "grant_type": "foo", + }, + wantCode: http.StatusBadRequest, + json: `{"error":"unsupported_grant_type","error_description":"foo not supported"}`, + }, + { + name: "introspection", + method: http.MethodGet, + path: testProvider.IntrospectionEndpoint().Relative(), + basicAuth: &basicAuth{"web", "secret"}, + values: map[string]string{ + "token": accessToken, + }, + wantCode: http.StatusOK, + json: `{"active":true,"scope":"openid offline_access email profile phone","client_id":"web","sub":"id1","username":"test-user@localhost","name":"Test User","given_name":"Test","family_name":"User","locale":"de","preferred_username":"test-user@localhost","email":"test-user@zitadel.ch","email_verified":true}`, + }, + { + name: "user info", + method: http.MethodGet, + path: testProvider.UserinfoEndpoint().Relative(), + header: map[string]string{ + "authorization": "Bearer " + accessToken, + }, + wantCode: http.StatusOK, + json: `{"sub":"id1","name":"Test User","given_name":"Test","family_name":"User","locale":"de","preferred_username":"test-user@localhost","email":"test-user@zitadel.ch","email_verified":true}`, + }, + { + name: "refresh token", + method: http.MethodGet, + path: testProvider.TokenEndpoint().Relative(), + values: map[string]string{ + "grant_type": string(oidc.GrantTypeRefreshToken), + "refresh_token": refreshToken, + "client_id": client.GetID(), + "client_secret": "secret", + }, + wantCode: http.StatusOK, + contains: []string{ + `{"access_token":"`, + `","token_type":"Bearer","refresh_token":"`, + `","expires_in":299,"id_token":"`, + }, + }, + { + name: "revoke", + method: http.MethodGet, + path: testProvider.RevocationEndpoint().Relative(), + basicAuth: &basicAuth{"web", "secret"}, + values: map[string]string{ + "token": accessTokenRevoke, + }, + wantCode: http.StatusOK, + }, + { + name: "end session", + method: http.MethodGet, + path: testProvider.EndSessionEndpoint().Relative(), + values: map[string]string{ + "id_token_hint": idToken, + "client_id": "web", + }, + wantCode: http.StatusFound, + headerContains: map[string]string{"Location": "/logged-out"}, + contains: []string{`Found.`}, + }, + { + name: "keys", + method: http.MethodGet, + path: testProvider.KeysEndpoint().Relative(), + wantCode: http.StatusOK, + contains: []string{ + `{"keys":[{"use":"sig","kty":"RSA","kid":"`, + `","alg":"RS256","n":"`, `","e":"AQAB"}]}`, + }, + }, + { + name: "device authorization", + method: http.MethodGet, + path: testProvider.DeviceAuthorizationEndpoint().Relative(), + basicAuth: &basicAuth{"web", "secret"}, + values: map[string]string{ + "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.String(), + }, + wantCode: http.StatusOK, + contains: []string{ + `{"device_code":"`, `","user_code":"`, + `","verification_uri":"https://localhost:9998/device"`, + `"verification_uri_complete":"https://localhost:9998/device?user_code=`, + `","expires_in":300,"interval":5}`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u := gu.PtrCopy(serverURL) + u.Path = tt.path + if tt.values != nil { + u.RawQuery = mapAsValues(tt.values) + } + var body io.Reader + if tt.body != nil { + body = strings.NewReader(mapAsValues(tt.body)) + } + + req := httptest.NewRequest(tt.method, u.String(), body) + for k, v := range tt.header { + req.Header.Set(k, v) + } + if tt.basicAuth != nil { + req.SetBasicAuth(tt.basicAuth.username, tt.basicAuth.password) + } + + rec := httptest.NewRecorder() + server.ServeHTTP(rec, req) + + resp := rec.Result() + require.NoError(t, err) + assert.Equal(t, tt.wantCode, resp.StatusCode) + + respBody, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + respBodyString := string(respBody) + t.Log(respBodyString) + t.Log(resp.Header) + + if tt.json != "" { + assert.JSONEq(t, tt.json, respBodyString) + } + for _, c := range tt.contains { + assert.Contains(t, respBodyString, c) + } + for k, v := range tt.headerContains { + assert.Contains(t, resp.Header.Get(k), v) + } + }) + } +} diff --git a/pkg/op/server_http_test.go b/pkg/op/server_http_test.go new file mode 100644 index 0000000..86fe7ed --- /dev/null +++ b/pkg/op/server_http_test.go @@ -0,0 +1,1333 @@ +package op + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "strings" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/schema" + "golang.org/x/exp/slog" +) + +func TestRegisterServer(t *testing.T) { + server := UnimplementedServer{} + endpoints := Endpoints{ + Authorization: &Endpoint{ + path: "/auth", + }, + } + decoder := schema.NewDecoder() + logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + + h := RegisterServer(server, endpoints, + WithDecoder(decoder), + WithFallbackLogger(logger), + ) + got := h.(*webServer) + assert.Equal(t, got.server, server) + assert.Equal(t, got.endpoints, endpoints) + assert.Equal(t, got.decoder, decoder) + assert.Equal(t, got.logger, logger) +} + +type testClient struct { + id string + appType ApplicationType + authMethod oidc.AuthMethod + accessTokenType AccessTokenType + responseTypes []oidc.ResponseType + grantTypes []oidc.GrantType + devMode bool +} + +type clientType string + +const ( + clientTypeWeb clientType = "web" + clientTypeNative clientType = "native" + clientTypeUserAgent clientType = "useragent" +) + +func newClient(kind clientType) *testClient { + client := &testClient{ + id: string(kind), + } + + switch kind { + case clientTypeWeb: + client.appType = ApplicationTypeWeb + client.authMethod = oidc.AuthMethodBasic + client.accessTokenType = AccessTokenTypeBearer + client.responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode} + case clientTypeNative: + client.appType = ApplicationTypeNative + client.authMethod = oidc.AuthMethodNone + client.accessTokenType = AccessTokenTypeBearer + client.responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode} + case clientTypeUserAgent: + client.appType = ApplicationTypeUserAgent + client.authMethod = oidc.AuthMethodBasic + client.accessTokenType = AccessTokenTypeJWT + client.responseTypes = []oidc.ResponseType{oidc.ResponseTypeIDToken} + default: + panic(fmt.Errorf("invalid client type %s", kind)) + } + return client +} + +func (c *testClient) RedirectURIs() []string { + return []string{ + "https://registered.com/callback", + "http://registered.com/callback", + "http://localhost:9999/callback", + "custom://callback", + } +} + +func (c *testClient) PostLogoutRedirectURIs() []string { + return []string{} +} + +func (c *testClient) LoginURL(id string) string { + return "login?id=" + id +} + +func (c *testClient) ApplicationType() ApplicationType { + return c.appType +} + +func (c *testClient) AuthMethod() oidc.AuthMethod { + return c.authMethod +} + +func (c *testClient) GetID() string { + return c.id +} + +func (c *testClient) AccessTokenLifetime() time.Duration { + return 5 * time.Minute +} + +func (c *testClient) IDTokenLifetime() time.Duration { + return 5 * time.Minute +} + +func (c *testClient) AccessTokenType() AccessTokenType { + return c.accessTokenType +} + +func (c *testClient) ResponseTypes() []oidc.ResponseType { + return c.responseTypes +} + +func (c *testClient) GrantTypes() []oidc.GrantType { + return c.grantTypes +} + +func (c *testClient) DevMode() bool { + return c.devMode +} + +func (c *testClient) AllowedScopes() []string { + return nil +} + +func (c *testClient) RestrictAdditionalIdTokenScopes() func(scopes []string) []string { + return func(scopes []string) []string { + return scopes + } +} + +func (c *testClient) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { + return func(scopes []string) []string { + return scopes + } +} + +func (c *testClient) IsScopeAllowed(scope string) bool { + return false +} + +func (c *testClient) IDTokenUserinfoClaimsAssertion() bool { + return false +} + +func (c *testClient) ClockSkew() time.Duration { + return 0 +} + +type requestVerifier struct { + UnimplementedServer + client Client +} + +func (s *requestVerifier) VerifyAuthRequest(ctx context.Context, r *Request[oidc.AuthRequest]) (*ClientRequest[oidc.AuthRequest], error) { + if s.client == nil { + return nil, oidc.ErrServerError() + } + return &ClientRequest[oidc.AuthRequest]{ + Request: r, + Client: s.client, + }, nil +} + +func (s *requestVerifier) VerifyClient(ctx context.Context, r *Request[ClientCredentials]) (Client, error) { + if s.client == nil { + return nil, oidc.ErrServerError() + } + return s.client, nil +} + +var testDecoder = func() *schema.Decoder { + decoder := schema.NewDecoder() + decoder.IgnoreUnknownKeys(true) + return decoder +}() + +type webServerResult struct { + wantStatus int + wantBody string +} + +func runWebServerTest(t *testing.T, handler http.HandlerFunc, r *http.Request, want webServerResult) { + t.Helper() + if r.Method == http.MethodPost { + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } + w := httptest.NewRecorder() + handler(w, r) + res := w.Result() + assert.Equal(t, want.wantStatus, res.StatusCode) + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.JSONEq(t, want.wantBody, string(body)) +} + +func Test_webServer_withClient(t *testing.T) { + tests := []struct { + name string + r *http.Request + want webServerResult + }{ + { + name: "parse error", + r: httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(make([]byte, 11<<20))), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"error parsing form"}`, + }, + }, + { + name: "invalid grant type", + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("client_id=native&grant_type=bad&foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"unauthorized_client", "error_description":"grant_type \"bad\" not allowed"}`, + }, + }, + { + name: "no grant type", + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("client_id=native&foo=bar")), + want: webServerResult{ + wantStatus: http.StatusOK, + wantBody: `{"foo":"bar"}`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &webServer{ + server: &requestVerifier{ + client: newClient(clientTypeNative), + }, + decoder: testDecoder, + logger: slog.Default(), + } + handler := func(w http.ResponseWriter, r *http.Request, client Client) { + fmt.Fprintf(w, `{"foo":%q}`, r.FormValue("foo")) + } + runWebServerTest(t, s.withClient(handler), tt.r, tt.want) + }) + } +} + +func Test_webServer_verifyRequestClient(t *testing.T) { + tests := []struct { + name string + decoder httphelper.Decoder + r *http.Request + want Client + wantErr error + }{ + { + name: "parse form error", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(make([]byte, 11<<20))), + wantErr: oidc.ErrInvalidRequest().WithDescription("error parsing form"), + }, + { + name: "decoder error", + decoder: schema.NewDecoder(), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + wantErr: oidc.ErrInvalidRequest().WithDescription("error decoding form"), + }, + { + name: "basic auth, client_id error", + decoder: testDecoder, + r: func() *http.Request { + r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")) + r.SetBasicAuth(`%%%`, "secret") + return r + }(), + wantErr: oidc.ErrInvalidClient().WithDescription("invalid basic auth header"), + }, + { + name: "basic auth, client_secret error", + decoder: testDecoder, + r: func() *http.Request { + r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")) + r.SetBasicAuth("web", `%%%`) + return r + }(), + wantErr: oidc.ErrInvalidClient().WithDescription("invalid basic auth header"), + }, + { + name: "missing client id and assertion", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + wantErr: oidc.ErrInvalidRequest().WithDescription("client_id or client_assertion must be provided"), + }, + { + name: "wrong assertion type", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar&client_assertion=xxx&client_assertion_type=wrong")), + wantErr: oidc.ErrInvalidRequest().WithDescription("invalid client_assertion_type wrong"), + }, + { + name: "unimplemented verify client called", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar&client_id=web")), + wantErr: StatusError{ + parent: oidc.ErrServerError().WithDescription("/ not implemented on this server"), + statusCode: UnimplementedStatusCode, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &webServer{ + server: UnimplementedServer{}, + decoder: tt.decoder, + logger: slog.Default(), + } + tt.r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + got, err := s.verifyRequestClient(tt.r) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_webServer_authorizeHandler(t *testing.T) { + type fields struct { + server Server + decoder httphelper.Decoder + } + tests := []struct { + name string + fields fields + r *http.Request + want webServerResult + }{ + { + name: "decoder error", + fields: fields{ + server: &requestVerifier{}, + decoder: schema.NewDecoder(), + }, + r: httptest.NewRequest(http.MethodPost, "/authorize", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"error decoding form"}`, + }, + }, + { + name: "authorize error", + fields: fields{ + server: &requestVerifier{}, + decoder: testDecoder, + }, + r: httptest.NewRequest(http.MethodPost, "/authorize", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"server_error"}`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &webServer{ + server: tt.fields.server, + decoder: tt.fields.decoder, + logger: slog.Default(), + } + runWebServerTest(t, s.authorizeHandler, tt.r, tt.want) + }) + } +} + +func Test_webServer_authorize(t *testing.T) { + type args struct { + ctx context.Context + r *Request[oidc.AuthRequest] + } + tests := []struct { + name string + server Server + args args + want *Redirect + wantErr error + }{ + { + name: "verify error", + server: &requestVerifier{}, + args: args{ + ctx: context.Background(), + r: &Request[oidc.AuthRequest]{ + Data: &oidc.AuthRequest{ + Scopes: oidc.SpaceDelimitedArray{"openid"}, + ResponseType: oidc.ResponseTypeCode, + ClientID: "web", + RedirectURI: "https://registered.com/callback", + MaxAge: gu.Ptr[uint](300), + }, + }, + }, + wantErr: oidc.ErrServerError(), + }, + { + name: "missing redirect", + server: &requestVerifier{ + client: newClient(clientTypeWeb), + }, + args: args{ + ctx: context.Background(), + r: &Request[oidc.AuthRequest]{ + Data: &oidc.AuthRequest{ + Scopes: oidc.SpaceDelimitedArray{"openid"}, + ResponseType: oidc.ResponseTypeCode, + ClientID: "web", + MaxAge: gu.Ptr[uint](300), + }, + }, + }, + wantErr: ErrAuthReqMissingRedirectURI, + }, + { + name: "invalid prompt", + server: &requestVerifier{ + client: newClient(clientTypeWeb), + }, + args: args{ + ctx: context.Background(), + r: &Request[oidc.AuthRequest]{ + Data: &oidc.AuthRequest{ + Scopes: oidc.SpaceDelimitedArray{"openid"}, + ResponseType: oidc.ResponseTypeCode, + ClientID: "web", + RedirectURI: "https://registered.com/callback", + MaxAge: gu.Ptr[uint](300), + Prompt: []string{oidc.PromptNone, oidc.PromptLogin}, + }, + }, + }, + wantErr: oidc.ErrInvalidRequest().WithDescription("The prompt parameter `none` must only be used as a single value"), + }, + { + name: "missing scopes", + server: &requestVerifier{ + client: newClient(clientTypeWeb), + }, + args: args{ + ctx: context.Background(), + r: &Request[oidc.AuthRequest]{ + Data: &oidc.AuthRequest{ + ResponseType: oidc.ResponseTypeCode, + ClientID: "web", + RedirectURI: "https://registered.com/callback", + MaxAge: gu.Ptr[uint](300), + Prompt: []string{oidc.PromptNone}, + }, + }, + }, + wantErr: oidc.ErrInvalidRequest(). + WithDescription("The scope of your request is missing. Please ensure some scopes are requested. " + + "If you have any questions, you may contact the administrator of the application."), + }, + { + name: "invalid redirect", + server: &requestVerifier{ + client: newClient(clientTypeWeb), + }, + args: args{ + ctx: context.Background(), + r: &Request[oidc.AuthRequest]{ + Data: &oidc.AuthRequest{ + Scopes: oidc.SpaceDelimitedArray{"openid"}, + ResponseType: oidc.ResponseTypeCode, + ClientID: "web", + RedirectURI: "https://example.com/callback", + MaxAge: gu.Ptr[uint](300), + Prompt: []string{oidc.PromptNone}, + }, + }, + }, + wantErr: oidc.ErrInvalidRequestRedirectURI(). + WithDescription("The requested redirect_uri is missing in the client configuration. " + + "If you have any questions, you may contact the administrator of the application."), + }, + { + name: "invalid response type", + server: &requestVerifier{ + client: newClient(clientTypeWeb), + }, + args: args{ + ctx: context.Background(), + r: &Request[oidc.AuthRequest]{ + Data: &oidc.AuthRequest{ + Scopes: oidc.SpaceDelimitedArray{"openid"}, + ResponseType: oidc.ResponseTypeIDToken, + ClientID: "web", + RedirectURI: "https://registered.com/callback", + MaxAge: gu.Ptr[uint](300), + Prompt: []string{oidc.PromptNone}, + }, + }, + }, + wantErr: oidc.ErrUnauthorizedClient().WithDescription("The requested response type is missing in the client configuration. " + + "If you have any questions, you may contact the administrator of the application."), + }, + { + name: "unimplemented Authorize called", + server: &requestVerifier{ + client: newClient(clientTypeWeb), + }, + args: args{ + ctx: context.Background(), + r: &Request[oidc.AuthRequest]{ + URL: &url.URL{ + Path: "/authorize", + }, + Data: &oidc.AuthRequest{ + Scopes: oidc.SpaceDelimitedArray{"openid"}, + ResponseType: oidc.ResponseTypeCode, + ClientID: "web", + RedirectURI: "https://registered.com/callback", + MaxAge: gu.Ptr[uint](300), + Prompt: []string{oidc.PromptNone}, + }, + }, + }, + wantErr: StatusError{ + parent: oidc.ErrServerError().WithDescription("/authorize not implemented on this server"), + statusCode: UnimplementedStatusCode, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &webServer{ + server: tt.server, + decoder: testDecoder, + logger: slog.Default(), + } + got, err := s.authorize(tt.args.ctx, tt.args.r) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_webServer_deviceAuthorizationHandler(t *testing.T) { + type fields struct { + server Server + decoder httphelper.Decoder + } + tests := []struct { + name string + fields fields + r *http.Request + want webServerResult + }{ + { + name: "decoder error", + fields: fields{ + server: &requestVerifier{}, + decoder: schema.NewDecoder(), + }, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"error decoding form"}`, + }, + }, + { + name: "unimplemented DeviceAuthorization called", + fields: fields{ + server: &requestVerifier{ + client: newClient(clientTypeNative), + }, + decoder: testDecoder, + }, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("client_id=native_client")), + want: webServerResult{ + wantStatus: UnimplementedStatusCode, + wantBody: `{"error":"server_error", "error_description":"/ not implemented on this server"}`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &webServer{ + server: tt.fields.server, + decoder: tt.fields.decoder, + logger: slog.Default(), + } + client := newClient(clientTypeUserAgent) + runWebServerClientTest(t, s.deviceAuthorizationHandler, tt.r, client, tt.want) + }) + } +} + +func Test_webServer_tokensHandler(t *testing.T) { + tests := []struct { + name string + r *http.Request + want webServerResult + }{ + { + name: "parse form error", + r: httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(make([]byte, 11<<20))), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"error parsing form"}`, + }, + }, + { + name: "missing grant type", + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"grant_type missing"}`, + }, + }, + { + name: "invalid grant type", + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("grant_type=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"unsupported_grant_type", "error_description":"bar not supported"}`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &webServer{ + logger: slog.Default(), + } + runWebServerTest(t, s.tokensHandler, tt.r, tt.want) + }) + } +} + +func Test_webServer_jwtProfileHandler(t *testing.T) { + tests := []struct { + name string + decoder httphelper.Decoder + r *http.Request + want webServerResult + }{ + { + name: "decoder error", + decoder: schema.NewDecoder(), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"error decoding form"}`, + }, + }, + { + name: "assertion missing", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"assertion missing"}`, + }, + }, + { + name: "unimplemented JWTProfile called", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("assertion=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"unsupported_grant_type", "error_description":"urn:ietf:params:oauth:grant-type:jwt-bearer not supported"}`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &webServer{ + server: UnimplementedServer{}, + decoder: tt.decoder, + logger: slog.Default(), + } + runWebServerTest(t, s.jwtProfileHandler, tt.r, tt.want) + }) + } +} + +func runWebServerClientTest(t *testing.T, handler func(http.ResponseWriter, *http.Request, Client), r *http.Request, client Client, want webServerResult) { + t.Helper() + runWebServerTest(t, func(client Client) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + handler(w, r, client) + } + }(client), r, want) +} + +func Test_webServer_codeExchangeHandler(t *testing.T) { + tests := []struct { + name string + decoder httphelper.Decoder + r *http.Request + want webServerResult + }{ + { + name: "decoder error", + decoder: schema.NewDecoder(), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"error decoding form"}`, + }, + }, + { + name: "code missing", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"code missing"}`, + }, + }, + { + name: "redirect missing", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("code=123")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"redirect_uri missing"}`, + }, + }, + { + name: "unimplemented CodeExchange called", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("code=123&redirect_uri=https://example.com/callback")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"unsupported_grant_type", "error_description":"authorization_code not supported"}`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &webServer{ + server: UnimplementedServer{}, + decoder: tt.decoder, + logger: slog.Default(), + } + client := newClient(clientTypeUserAgent) + runWebServerClientTest(t, s.codeExchangeHandler, tt.r, client, tt.want) + }) + } +} + +func Test_webServer_refreshTokenHandler(t *testing.T) { + tests := []struct { + name string + decoder httphelper.Decoder + r *http.Request + want webServerResult + }{ + { + name: "decoder error", + decoder: schema.NewDecoder(), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"error decoding form"}`, + }, + }, + { + name: "refresh token missing", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"refresh_token missing"}`, + }, + }, + { + name: "unimplemented RefreshToken called", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("refresh_token=xxx")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"unsupported_grant_type", "error_description":"refresh_token not supported"}`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &webServer{ + server: UnimplementedServer{}, + decoder: tt.decoder, + logger: slog.Default(), + } + client := newClient(clientTypeUserAgent) + runWebServerClientTest(t, s.refreshTokenHandler, tt.r, client, tt.want) + }) + } +} + +func Test_webServer_tokenExchangeHandler(t *testing.T) { + tests := []struct { + name string + decoder httphelper.Decoder + r *http.Request + want webServerResult + }{ + { + name: "decoder error", + decoder: schema.NewDecoder(), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"error decoding form"}`, + }, + }, + { + name: "subject token missing", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"subject_token missing"}`, + }, + }, + { + name: "subject token type missing", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("subject_token=xxx")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"subject_token_type missing"}`, + }, + }, + { + name: "subject token type unsupported", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("subject_token=xxx&subject_token_type=foo")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"subject_token_type is not supported"}`, + }, + }, + { + name: "unsupported requested token type", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("subject_token=xxx&subject_token_type=urn:ietf:params:oauth:token-type:access_token&requested_token_type=foo")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"requested_token_type is not supported"}`, + }, + }, + { + name: "unsupported actor token type", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("subject_token=xxx&subject_token_type=urn:ietf:params:oauth:token-type:access_token&requested_token_type=urn:ietf:params:oauth:token-type:access_token&actor_token_type=foo")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"actor_token_type is not supported"}`, + }, + }, + { + name: "unimplemented TokenExchange called", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("subject_token=xxx&subject_token_type=urn:ietf:params:oauth:token-type:access_token&requested_token_type=urn:ietf:params:oauth:token-type:access_token&actor_token_type=urn:ietf:params:oauth:token-type:access_token")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"unsupported_grant_type", "error_description":"urn:ietf:params:oauth:grant-type:token-exchange not supported"}`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &webServer{ + server: UnimplementedServer{}, + decoder: tt.decoder, + logger: slog.Default(), + } + client := newClient(clientTypeUserAgent) + runWebServerClientTest(t, s.tokenExchangeHandler, tt.r, client, tt.want) + }) + } +} + +func Test_webServer_clientCredentialsHandler(t *testing.T) { + tests := []struct { + name string + decoder httphelper.Decoder + client Client + r *http.Request + want webServerResult + }{ + { + name: "decoder error", + decoder: schema.NewDecoder(), + client: newClient(clientTypeUserAgent), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"error decoding form"}`, + }, + }, + { + name: "public client", + decoder: testDecoder, + client: newClient(clientTypeNative), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_client", "error_description":"client must be authenticated"}`, + }, + }, + { + name: "unimplemented ClientCredentialsExchange called", + decoder: testDecoder, + client: newClient(clientTypeUserAgent), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"unsupported_grant_type", "error_description":"client_credentials not supported"}`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &webServer{ + server: UnimplementedServer{}, + decoder: tt.decoder, + logger: slog.Default(), + } + runWebServerClientTest(t, s.clientCredentialsHandler, tt.r, tt.client, tt.want) + }) + } +} + +func Test_webServer_deviceTokenHandler(t *testing.T) { + tests := []struct { + name string + decoder httphelper.Decoder + r *http.Request + want webServerResult + }{ + { + name: "decoder error", + decoder: schema.NewDecoder(), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"error decoding form"}`, + }, + }, + { + name: "device code missing", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"device_code missing"}`, + }, + }, + { + name: "unimplemented DeviceToken called", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("device_code=xxx")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"unsupported_grant_type", "error_description":"urn:ietf:params:oauth:grant-type:device_code not supported"}`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &webServer{ + server: UnimplementedServer{}, + decoder: tt.decoder, + logger: slog.Default(), + } + client := newClient(clientTypeUserAgent) + runWebServerClientTest(t, s.deviceTokenHandler, tt.r, client, tt.want) + }) + } +} + +func Test_webServer_introspectionHandler(t *testing.T) { + tests := []struct { + name string + decoder httphelper.Decoder + client Client + r *http.Request + want webServerResult + }{ + { + name: "decoder error", + decoder: schema.NewDecoder(), + client: newClient(clientTypeUserAgent), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"error decoding form"}`, + }, + }, + { + name: "public client", + decoder: testDecoder, + client: newClient(clientTypeNative), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_client", "error_description":"client must be authenticated"}`, + }, + }, + { + name: "token missing", + decoder: testDecoder, + client: newClient(clientTypeWeb), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"token missing"}`, + }, + }, + { + name: "unimplemented Introspect called", + decoder: testDecoder, + client: newClient(clientTypeWeb), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("token=xxx")), + want: webServerResult{ + wantStatus: UnimplementedStatusCode, + wantBody: `{"error":"server_error", "error_description":"/ not implemented on this server"}`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &webServer{ + server: UnimplementedServer{}, + decoder: tt.decoder, + logger: slog.Default(), + } + runWebServerClientTest(t, s.introspectionHandler, tt.r, tt.client, tt.want) + }) + } +} + +func Test_webServer_userInfoHandler(t *testing.T) { + tests := []struct { + name string + decoder httphelper.Decoder + r *http.Request + want webServerResult + }{ + { + name: "decoder error", + decoder: schema.NewDecoder(), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"error decoding form"}`, + }, + }, + { + name: "access token missing", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusUnauthorized, + wantBody: `{"error":"invalid_request", "error_description":"access token missing"}`, + }, + }, + { + name: "unimplemented UserInfo called", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("access_token=xxx")), + want: webServerResult{ + wantStatus: UnimplementedStatusCode, + wantBody: `{"error":"server_error", "error_description":"/ not implemented on this server"}`, + }, + }, + { + name: "bearer", + decoder: testDecoder, + r: func() *http.Request { + r := httptest.NewRequest(http.MethodGet, "/", nil) + r.Header.Set("authorization", strings.Join([]string{"Bearer", "xxx"}, " ")) + return r + }(), + want: webServerResult{ + wantStatus: UnimplementedStatusCode, + wantBody: `{"error":"server_error", "error_description":"/ not implemented on this server"}`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &webServer{ + server: UnimplementedServer{}, + decoder: tt.decoder, + logger: slog.Default(), + } + runWebServerTest(t, s.userInfoHandler, tt.r, tt.want) + }) + } +} + +func Test_webServer_revocationHandler(t *testing.T) { + tests := []struct { + name string + decoder httphelper.Decoder + client Client + r *http.Request + want webServerResult + }{ + { + name: "decoder error", + decoder: schema.NewDecoder(), + client: newClient(clientTypeWeb), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"error decoding form"}`, + }, + }, + { + name: "token missing", + decoder: testDecoder, + client: newClient(clientTypeWeb), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"token missing"}`, + }, + }, + { + name: "unimplemented Revocation called, confidential client", + decoder: testDecoder, + client: newClient(clientTypeWeb), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("token=xxx")), + want: webServerResult{ + wantStatus: UnimplementedStatusCode, + wantBody: `{"error":"server_error", "error_description":"/ not implemented on this server"}`, + }, + }, + { + name: "unimplemented Revocation called, public client", + decoder: testDecoder, + client: newClient(clientTypeNative), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("token=xxx")), + want: webServerResult{ + wantStatus: UnimplementedStatusCode, + wantBody: `{"error":"server_error", "error_description":"/ not implemented on this server"}`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &webServer{ + server: UnimplementedServer{}, + decoder: tt.decoder, + logger: slog.Default(), + } + runWebServerClientTest(t, s.revocationHandler, tt.r, tt.client, tt.want) + }) + } +} + +func Test_webServer_endSessionHandler(t *testing.T) { + tests := []struct { + name string + decoder httphelper.Decoder + r *http.Request + want webServerResult + }{ + { + name: "decoder error", + decoder: schema.NewDecoder(), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"error decoding form"}`, + }, + }, + { + name: "unimplemented EndSession called", + decoder: testDecoder, + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("id_token_hint=xxx")), + want: webServerResult{ + wantStatus: UnimplementedStatusCode, + wantBody: `{"error":"server_error", "error_description":"/ not implemented on this server"}`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &webServer{ + server: UnimplementedServer{}, + decoder: tt.decoder, + logger: slog.Default(), + } + runWebServerTest(t, s.endSessionHandler, tt.r, tt.want) + }) + } +} + +func Test_webServer_simpleHandler(t *testing.T) { + tests := []struct { + name string + decoder httphelper.Decoder + method func(context.Context, *Request[struct{}]) (*Response, error) + r *http.Request + want webServerResult + }{ + { + name: "parse error", + decoder: schema.NewDecoder(), + r: httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(make([]byte, 11<<20))), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"invalid_request", "error_description":"error parsing form"}`, + }, + }, + { + name: "method error", + decoder: schema.NewDecoder(), + method: func(ctx context.Context, r *Request[struct{}]) (*Response, error) { + return nil, io.ErrClosedPipe + }, + r: httptest.NewRequest(http.MethodGet, "/", bytes.NewReader(make([]byte, 11<<20))), + want: webServerResult{ + wantStatus: http.StatusBadRequest, + wantBody: `{"error":"server_error", "error_description":"io: read/write on closed pipe"}`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &webServer{ + server: UnimplementedServer{}, + decoder: tt.decoder, + logger: slog.Default(), + } + runWebServerTest(t, simpleHandler(s, tt.method), tt.r, tt.want) + }) + } +} + +func Test_decodeRequest(t *testing.T) { + type dst struct { + A string `schema:"a"` + B string `schema:"b"` + } + type args struct { + r *http.Request + postOnly bool + } + tests := []struct { + name string + args args + want *dst + wantErr error + }{ + { + name: "parse error", + args: args{ + r: httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(make([]byte, 11<<20))), + }, + wantErr: oidc.ErrInvalidRequest().WithDescription("error parsing form"), + }, + { + name: "decode error", + args: args{ + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + }, + wantErr: oidc.ErrInvalidRequest().WithDescription("error decoding form"), + }, + { + name: "success, get", + args: args{ + r: httptest.NewRequest(http.MethodGet, "/?a=b&b=a", nil), + }, + want: &dst{ + A: "b", + B: "a", + }, + }, + { + name: "success, post only", + args: args{ + r: httptest.NewRequest(http.MethodPost, "/?b=a", strings.NewReader("a=b&")), + postOnly: true, + }, + want: &dst{ + A: "b", + }, + }, + { + name: "success, post mixed", + args: args{ + r: httptest.NewRequest(http.MethodPost, "/?b=a", strings.NewReader("a=b&")), + postOnly: false, + }, + want: &dst{ + A: "b", + B: "a", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.r.Method == http.MethodPost { + tt.args.r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } + got, err := decodeRequest[dst](schema.NewDecoder(), tt.args.r, tt.args.postOnly) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/op/server_legacy.go b/pkg/op/server_legacy.go new file mode 100644 index 0000000..0a7de85 --- /dev/null +++ b/pkg/op/server_legacy.go @@ -0,0 +1,344 @@ +package op + +import ( + "context" + "errors" + "net/http" + "time" + + "github.com/go-chi/chi" + "github.com/zitadel/oidc/v3/pkg/oidc" +) + +// LegacyServer is an implementation of [Server[] that +// simply wraps a [OpenIDProvider]. +// It can be used to transition from the former Provider/Storage +// interfaces to the new Server interface. +type LegacyServer struct { + UnimplementedServer + provider OpenIDProvider + endpoints Endpoints +} + +// NewLegacyServer wraps provider in a `Server` and returns a handler which is +// the Server's router. +// +// Only non-nil endpoints will be registered on the router. +// Nil endpoints are disabled. +// +// The passed endpoints is also set to the provider, +// to be consistent with the discovery config. +// Any `With*Endpoint()` option used on the provider is +// therefore ineffective. +func NewLegacyServer(provider OpenIDProvider, endpoints Endpoints) http.Handler { + server := RegisterServer(&LegacyServer{ + provider: provider, + endpoints: endpoints, + }, endpoints, WithHTTPMiddleware(intercept(provider.IssuerFromRequest))) + + router := chi.NewRouter() + router.Mount("/", server) + router.HandleFunc(authCallbackPath(provider), authorizeCallbackHandler(provider)) + + return router +} + +func (s *LegacyServer) Health(_ context.Context, r *Request[struct{}]) (*Response, error) { + return NewResponse(Status{Status: "ok"}), nil +} + +func (s *LegacyServer) Ready(ctx context.Context, r *Request[struct{}]) (*Response, error) { + for _, probe := range s.provider.Probes() { + // shouldn't we run probes in Go routines? + if err := probe(ctx); err != nil { + return nil, NewStatusError(err, http.StatusInternalServerError) + } + } + return NewResponse(Status{Status: "ok"}), nil +} + +func (s *LegacyServer) Discovery(ctx context.Context, r *Request[struct{}]) (*Response, error) { + return NewResponse( + createDiscoveryConfigV2(ctx, s.provider, s.provider.Storage(), &s.endpoints), + ), nil +} + +func (s *LegacyServer) Keys(ctx context.Context, r *Request[struct{}]) (*Response, error) { + keys, err := s.provider.Storage().KeySet(ctx) + if err != nil { + return nil, NewStatusError(err, http.StatusInternalServerError) + } + return NewResponse(jsonWebKeySet(keys)), nil +} + +var ( + ErrAuthReqMissingClientID = errors.New("auth request is missing client_id") + ErrAuthReqMissingRedirectURI = errors.New("auth request is missing redirect_uri") +) + +func (s *LegacyServer) VerifyAuthRequest(ctx context.Context, r *Request[oidc.AuthRequest]) (*ClientRequest[oidc.AuthRequest], error) { + if r.Data.RequestParam != "" { + if !s.provider.RequestObjectSupported() { + return nil, oidc.ErrRequestNotSupported() + } + err := ParseRequestObject(ctx, r.Data, s.provider.Storage(), IssuerFromContext(ctx)) + if err != nil { + return nil, err + } + } + if r.Data.ClientID == "" { + return nil, ErrAuthReqMissingClientID + } + client, err := s.provider.Storage().GetClientByClientID(ctx, r.Data.ClientID) + if err != nil { + return nil, oidc.DefaultToServerError(err, "unable to retrieve client by id") + } + + return &ClientRequest[oidc.AuthRequest]{ + Request: r, + Client: client, + }, nil +} + +func (s *LegacyServer) Authorize(ctx context.Context, r *ClientRequest[oidc.AuthRequest]) (_ *Redirect, err error) { + userID, err := ValidateAuthReqIDTokenHint(ctx, r.Data.IDTokenHint, s.provider.IDTokenHintVerifier(ctx)) + if err != nil { + return nil, err + } + req, err := s.provider.Storage().CreateAuthRequest(ctx, r.Data, userID) + if err != nil { + return TryErrorRedirect(ctx, r.Data, oidc.DefaultToServerError(err, "unable to save auth request"), s.provider.Encoder(), s.provider.Logger()) + } + return NewRedirect(r.Client.LoginURL(req.GetID())), nil +} + +func (s *LegacyServer) DeviceAuthorization(ctx context.Context, r *ClientRequest[oidc.DeviceAuthorizationRequest]) (*Response, error) { + response, err := createDeviceAuthorization(ctx, r.Data, r.Client.GetID(), s.provider) + if err != nil { + return nil, NewStatusError(err, http.StatusInternalServerError) + } + return NewResponse(response), nil +} + +func (s *LegacyServer) VerifyClient(ctx context.Context, r *Request[ClientCredentials]) (Client, error) { + if oidc.GrantType(r.Form.Get("grant_type")) == oidc.GrantTypeClientCredentials { + storage, ok := s.provider.Storage().(ClientCredentialsStorage) + if !ok { + return nil, oidc.ErrUnsupportedGrantType().WithDescription("client_credentials grant not supported") + } + return storage.ClientCredentials(ctx, r.Data.ClientID, r.Data.ClientSecret) + } + + if r.Data.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion { + jwtExchanger, ok := s.provider.(JWTAuthorizationGrantExchanger) + if !ok || !s.provider.AuthMethodPrivateKeyJWTSupported() { + return nil, oidc.ErrInvalidClient().WithDescription("auth_method private_key_jwt not supported") + } + return AuthorizePrivateJWTKey(ctx, r.Data.ClientAssertion, jwtExchanger) + } + client, err := s.provider.Storage().GetClientByClientID(ctx, r.Data.ClientID) + if err != nil { + return nil, oidc.ErrInvalidClient().WithParent(err) + } + + switch client.AuthMethod() { + case oidc.AuthMethodNone: + return client, nil + case oidc.AuthMethodPrivateKeyJWT: + return nil, oidc.ErrInvalidClient().WithDescription("private_key_jwt not allowed for this client") + case oidc.AuthMethodPost: + if !s.provider.AuthMethodPostSupported() { + return nil, oidc.ErrInvalidClient().WithDescription("auth_method post not supported") + } + } + + err = AuthorizeClientIDSecret(ctx, r.Data.ClientID, r.Data.ClientSecret, s.provider.Storage()) + if err != nil { + return nil, err + } + + return client, nil +} + +func (s *LegacyServer) CodeExchange(ctx context.Context, r *ClientRequest[oidc.AccessTokenRequest]) (*Response, error) { + authReq, err := AuthRequestByCode(ctx, s.provider.Storage(), r.Data.Code) + if err != nil { + return nil, err + } + if r.Client.AuthMethod() == oidc.AuthMethodNone { + if err = AuthorizeCodeChallenge(r.Data.CodeVerifier, authReq.GetCodeChallenge()); err != nil { + return nil, err + } + } + resp, err := CreateTokenResponse(ctx, authReq, r.Client, s.provider, true, r.Data.Code, "") + if err != nil { + return nil, err + } + return NewResponse(resp), nil +} + +func (s *LegacyServer) RefreshToken(ctx context.Context, r *ClientRequest[oidc.RefreshTokenRequest]) (*Response, error) { + if !s.provider.GrantTypeRefreshTokenSupported() { + return nil, unimplementedGrantError(oidc.GrantTypeRefreshToken) + } + request, err := RefreshTokenRequestByRefreshToken(ctx, s.provider.Storage(), r.Data.RefreshToken) + if err != nil { + return nil, err + } + if r.Client.GetID() != request.GetClientID() { + return nil, oidc.ErrInvalidGrant() + } + if err = ValidateRefreshTokenScopes(r.Data.Scopes, request); err != nil { + return nil, err + } + resp, err := CreateTokenResponse(ctx, request, r.Client, s.provider, true, "", r.Data.RefreshToken) + if err != nil { + return nil, err + } + return NewResponse(resp), nil +} + +func (s *LegacyServer) JWTProfile(ctx context.Context, r *Request[oidc.JWTProfileGrantRequest]) (*Response, error) { + exchanger, ok := s.provider.(JWTAuthorizationGrantExchanger) + if !ok { + return nil, unimplementedGrantError(oidc.GrantTypeBearer) + } + tokenRequest, err := VerifyJWTAssertion(ctx, r.Data.Assertion, exchanger.JWTProfileVerifier(ctx)) + if err != nil { + return nil, err + } + + tokenRequest.Scopes, err = exchanger.Storage().ValidateJWTProfileScopes(ctx, tokenRequest.Issuer, r.Data.Scope) + if err != nil { + return nil, err + } + resp, err := CreateJWTTokenResponse(ctx, tokenRequest, exchanger) + if err != nil { + return nil, err + } + return NewResponse(resp), nil +} + +func (s *LegacyServer) TokenExchange(ctx context.Context, r *ClientRequest[oidc.TokenExchangeRequest]) (*Response, error) { + if !s.provider.GrantTypeTokenExchangeSupported() { + return nil, unimplementedGrantError(oidc.GrantTypeTokenExchange) + } + tokenExchangeRequest, err := CreateTokenExchangeRequest(ctx, r.Data, r.Client, s.provider) + if err != nil { + return nil, err + } + resp, err := CreateTokenExchangeResponse(ctx, tokenExchangeRequest, r.Client, s.provider) + if err != nil { + return nil, err + } + return NewResponse(resp), nil +} + +func (s *LegacyServer) ClientCredentialsExchange(ctx context.Context, r *ClientRequest[oidc.ClientCredentialsRequest]) (*Response, error) { + storage, ok := s.provider.Storage().(ClientCredentialsStorage) + if !ok { + return nil, unimplementedGrantError(oidc.GrantTypeClientCredentials) + } + tokenRequest, err := storage.ClientCredentialsTokenRequest(ctx, r.Client.GetID(), r.Data.Scope) + if err != nil { + return nil, err + } + resp, err := CreateClientCredentialsTokenResponse(ctx, tokenRequest, s.provider, r.Client) + if err != nil { + return nil, err + } + return NewResponse(resp), nil +} + +func (s *LegacyServer) DeviceToken(ctx context.Context, r *ClientRequest[oidc.DeviceAccessTokenRequest]) (*Response, error) { + if !s.provider.GrantTypeClientCredentialsSupported() { + return nil, unimplementedGrantError(oidc.GrantTypeDeviceCode) + } + // use a limited context timeout shorter as the default + // poll interval of 5 seconds. + ctx, cancel := context.WithTimeout(ctx, 4*time.Second) + defer cancel() + + state, err := CheckDeviceAuthorizationState(ctx, r.Client.GetID(), r.Data.DeviceCode, s.provider) + if err != nil { + return nil, err + } + tokenRequest := &deviceAccessTokenRequest{ + subject: state.Subject, + audience: []string{r.Client.GetID()}, + scopes: state.Scopes, + } + resp, err := CreateDeviceTokenResponse(ctx, tokenRequest, s.provider, r.Client) + if err != nil { + return nil, err + } + return NewResponse(resp), nil +} + +func (s *LegacyServer) Introspect(ctx context.Context, r *ClientRequest[oidc.IntrospectionRequest]) (*Response, error) { + response := new(oidc.IntrospectionResponse) + tokenID, subject, ok := getTokenIDAndSubject(ctx, s.provider, r.Data.Token) + if !ok { + return NewResponse(response), nil + } + err := s.provider.Storage().SetIntrospectionFromToken(ctx, response, tokenID, subject, r.Client.GetID()) + if err != nil { + return NewResponse(response), nil + } + response.Active = true + return NewResponse(response), nil +} + +func (s *LegacyServer) UserInfo(ctx context.Context, r *Request[oidc.UserInfoRequest]) (*Response, error) { + tokenID, subject, ok := getTokenIDAndSubject(ctx, s.provider, r.Data.AccessToken) + if !ok { + return nil, NewStatusError(oidc.ErrAccessDenied().WithDescription("access token invalid"), http.StatusUnauthorized) + } + info := new(oidc.UserInfo) + err := s.provider.Storage().SetUserinfoFromToken(ctx, info, tokenID, subject, r.Header.Get("origin")) + if err != nil { + return nil, NewStatusError(err, http.StatusForbidden) + } + return NewResponse(info), nil +} + +func (s *LegacyServer) Revocation(ctx context.Context, r *ClientRequest[oidc.RevocationRequest]) (*Response, error) { + var subject string + doDecrypt := true + if r.Data.TokenTypeHint != "access_token" { + userID, tokenID, err := s.provider.Storage().GetRefreshTokenInfo(ctx, r.Client.GetID(), r.Data.Token) + if err != nil { + // An invalid refresh token means that we'll try other things (leaving doDecrypt==true) + if !errors.Is(err, ErrInvalidRefreshToken) { + return nil, RevocationError(oidc.ErrServerError().WithParent(err)) + } + } else { + r.Data.Token = tokenID + subject = userID + doDecrypt = false + } + } + if doDecrypt { + tokenID, userID, ok := getTokenIDAndSubjectForRevocation(ctx, s.provider, r.Data.Token) + if ok { + r.Data.Token = tokenID + subject = userID + } + } + if err := s.provider.Storage().RevokeToken(ctx, r.Data.Token, subject, r.Client.GetID()); err != nil { + return nil, RevocationError(err) + } + return NewResponse(nil), nil +} + +func (s *LegacyServer) EndSession(ctx context.Context, r *Request[oidc.EndSessionRequest]) (*Redirect, error) { + session, err := ValidateEndSessionRequest(ctx, r.Data, s.provider) + if err != nil { + return nil, err + } + err = s.provider.Storage().TerminateSession(ctx, session.UserID, session.ClientID) + if err != nil { + return nil, err + } + return NewRedirect(session.RedirectURI), nil +} diff --git a/pkg/op/server_test.go b/pkg/op/server_test.go new file mode 100644 index 0000000..0cad8fd --- /dev/null +++ b/pkg/op/server_test.go @@ -0,0 +1,5 @@ +package op + +// implementation check +var _ Server = &UnimplementedServer{} +var _ Server = &LegacyServer{} diff --git a/pkg/op/token_code.go b/pkg/op/token_code.go index baf377b..371e1d4 100644 --- a/pkg/op/token_code.go +++ b/pkg/op/token_code.go @@ -88,7 +88,7 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, if err != nil { return nil, nil, err } - err = AuthorizeCodeChallenge(tokenReq, request.GetCodeChallenge()) + err = AuthorizeCodeChallenge(tokenReq.CodeVerifier, request.GetCodeChallenge()) return request, client, err } if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() { diff --git a/pkg/op/token_exchange.go b/pkg/op/token_exchange.go index 21db134..5156741 100644 --- a/pkg/op/token_exchange.go +++ b/pkg/op/token_exchange.go @@ -197,12 +197,6 @@ func ValidateTokenExchangeRequest( return nil, nil, oidc.ErrInvalidRequest().WithDescription("subject_token_type missing") } - storage := exchanger.Storage() - teStorage, ok := storage.(TokenExchangeStorage) - if !ok { - return nil, nil, oidc.ErrUnsupportedGrantType().WithDescription("token_exchange grant not supported") - } - client, err := AuthorizeTokenExchangeClient(ctx, clientID, clientSecret, exchanger) if err != nil { return nil, nil, err @@ -220,10 +214,28 @@ func ValidateTokenExchangeRequest( return nil, nil, oidc.ErrInvalidRequest().WithDescription("actor_token_type is not supported") } + req, err := CreateTokenExchangeRequest(ctx, oidcTokenExchangeRequest, client, exchanger) + if err != nil { + return nil, nil, err + } + return req, client, nil +} + +func CreateTokenExchangeRequest( + ctx context.Context, + oidcTokenExchangeRequest *oidc.TokenExchangeRequest, + client Client, + exchanger Exchanger, +) (TokenExchangeRequest, error) { + teStorage, ok := exchanger.Storage().(TokenExchangeStorage) + if !ok { + return nil, unimplementedGrantError(oidc.GrantTypeTokenExchange) + } + exchangeSubjectTokenIDOrToken, exchangeSubject, exchangeSubjectTokenClaims, ok := GetTokenIDAndSubjectFromToken(ctx, exchanger, oidcTokenExchangeRequest.SubjectToken, oidcTokenExchangeRequest.SubjectTokenType, false) if !ok { - return nil, nil, oidc.ErrInvalidRequest().WithDescription("subject_token is invalid") + return nil, oidc.ErrInvalidRequest().WithDescription("subject_token is invalid") } var ( @@ -234,7 +246,7 @@ func ValidateTokenExchangeRequest( exchangeActorTokenIDOrToken, exchangeActor, exchangeActorTokenClaims, ok = GetTokenIDAndSubjectFromToken(ctx, exchanger, oidcTokenExchangeRequest.ActorToken, oidcTokenExchangeRequest.ActorTokenType, true) if !ok { - return nil, nil, oidc.ErrInvalidRequest().WithDescription("actor_token is invalid") + return nil, oidc.ErrInvalidRequest().WithDescription("actor_token is invalid") } } @@ -258,17 +270,17 @@ func ValidateTokenExchangeRequest( authTime: time.Now(), } - err = teStorage.ValidateTokenExchangeRequest(ctx, req) + err := teStorage.ValidateTokenExchangeRequest(ctx, req) if err != nil { - return nil, nil, err + return nil, err } err = teStorage.CreateTokenExchangeRequest(ctx, req) if err != nil { - return nil, nil, err + return nil, err } - return req, client, nil + return req, nil } func GetTokenIDAndSubjectFromToken( diff --git a/pkg/op/token_request.go b/pkg/op/token_request.go index 0df2fce..b810633 100644 --- a/pkg/op/token_request.go +++ b/pkg/op/token_request.go @@ -117,11 +117,11 @@ func AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string, // AuthorizeCodeChallenge authorizes a client by validating the code_verifier against the previously sent // code_challenge of the auth request (PKCE) -func AuthorizeCodeChallenge(tokenReq *oidc.AccessTokenRequest, challenge *oidc.CodeChallenge) error { - if tokenReq.CodeVerifier == "" { +func AuthorizeCodeChallenge(codeVerifier string, challenge *oidc.CodeChallenge) error { + if codeVerifier == "" { return oidc.ErrInvalidRequest().WithDescription("code_challenge required") } - if !oidc.VerifyCodeChallenge(challenge, tokenReq.CodeVerifier) { + if !oidc.VerifyCodeChallenge(challenge, codeVerifier) { return oidc.ErrInvalidGrant().WithDescription("invalid code challenge") } return nil diff --git a/pkg/op/token_revocation.go b/pkg/op/token_revocation.go index fd1ee93..d19c7f7 100644 --- a/pkg/op/token_revocation.go +++ b/pkg/op/token_revocation.go @@ -131,6 +131,11 @@ func ParseTokenRevocationRequest(r *http.Request, revoker Revoker) (token, token } func RevocationRequestError(w http.ResponseWriter, r *http.Request, err error) { + statusErr := RevocationError(err) + httphelper.MarshalJSONWithStatus(w, statusErr.parent, statusErr.statusCode) +} + +func RevocationError(err error) StatusError { e := oidc.DefaultToServerError(err, err.Error()) status := http.StatusBadRequest switch e.ErrorType { @@ -139,7 +144,7 @@ func RevocationRequestError(w http.ResponseWriter, r *http.Request, err error) { case oidc.ServerError: status = 500 } - httphelper.MarshalJSONWithStatus(w, e, status) + return NewStatusError(e, status) } func getTokenIDAndSubjectForRevocation(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, bool) { From 8488cb054b556fcf0665d58f360170bad09fdc6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Oct 2023 07:56:52 +0200 Subject: [PATCH 286/502] chore(deps): bump golang.org/x/net from 0.15.0 to 0.17.0 (#455) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.15.0 to 0.17.0. - [Commits](https://github.com/golang/net/compare/v0.15.0...v0.17.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 5923722..d4b8f09 100644 --- a/go.mod +++ b/go.mod @@ -30,9 +30,9 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/otel/metric v1.18.0 // indirect - golang.org/x/crypto v0.13.0 // indirect - golang.org/x/net v0.15.0 // indirect - golang.org/x/sys v0.12.0 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 2b0e678..f0cbf28 100644 --- a/go.sum +++ b/go.sum @@ -56,8 +56,8 @@ go.opentelemetry.io/otel/trace v1.18.0 h1:NY+czwbHbmndxojTEKiSMHkG2ClNH2PwmcHrdo go.opentelemetry.io/otel/trace v1.18.0/go.mod h1:T2+SGJGuYZY3bjj5rgh/hN7KIrlpWC5nS8Mjvzckz+0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -65,8 +65,8 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= @@ -78,8 +78,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= From ceaf2b184dcb36874998b06a5a804e5ae13c88c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Oct 2023 11:55:35 +0300 Subject: [PATCH 287/502] chore(deps): bump go.opentelemetry.io/otel/trace from 1.18.0 to 1.19.0 (#449) Bumps [go.opentelemetry.io/otel/trace](https://github.com/open-telemetry/opentelemetry-go) from 1.18.0 to 1.19.0. - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.18.0...v1.19.0) --- updated-dependencies: - dependency-name: go.opentelemetry.io/otel/trace dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index d4b8f09..3f75819 100644 --- a/go.mod +++ b/go.mod @@ -15,8 +15,8 @@ require ( github.com/rs/cors v1.10.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 - go.opentelemetry.io/otel v1.18.0 - go.opentelemetry.io/otel/trace v1.18.0 + go.opentelemetry.io/otel v1.19.0 + go.opentelemetry.io/otel/trace v1.19.0 golang.org/x/oauth2 v0.12.0 golang.org/x/text v0.13.0 gopkg.in/square/go-jose.v2 v2.6.0 @@ -29,7 +29,7 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel/metric v1.18.0 // indirect + go.opentelemetry.io/otel/metric v1.19.0 // indirect golang.org/x/crypto v0.14.0 // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/sys v0.13.0 // indirect diff --git a/go.sum b/go.sum index f0cbf28..89cea25 100644 --- a/go.sum +++ b/go.sum @@ -48,12 +48,12 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.opentelemetry.io/otel v1.18.0 h1:TgVozPGZ01nHyDZxK5WGPFB9QexeTMXEH7+tIClWfzs= -go.opentelemetry.io/otel v1.18.0/go.mod h1:9lWqYO0Db579XzVuCKFNPDl4s73Voa+zEck3wHaAYQI= -go.opentelemetry.io/otel/metric v1.18.0 h1:JwVzw94UYmbx3ej++CwLUQZxEODDj/pOuTCvzhtRrSQ= -go.opentelemetry.io/otel/metric v1.18.0/go.mod h1:nNSpsVDjWGfb7chbRLUNW+PBNdcSTHD4Uu5pfFMOI0k= -go.opentelemetry.io/otel/trace v1.18.0 h1:NY+czwbHbmndxojTEKiSMHkG2ClNH2PwmcHrdo0JY10= -go.opentelemetry.io/otel/trace v1.18.0/go.mod h1:T2+SGJGuYZY3bjj5rgh/hN7KIrlpWC5nS8Mjvzckz+0= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= From e6e3835362bad0cb78ab45b0e6132d708e81249e Mon Sep 17 00:00:00 2001 From: Thomas Hipp Date: Thu, 12 Oct 2023 11:41:04 +0200 Subject: [PATCH 288/502] chore: replace `interface{}` with `any` (#448) This PR replaces all occurances of interface{} with any to be consistent and improve readability. * example: Replace `interface{}` with `any` Signed-off-by: Thomas Hipp * pkg/client: Replace `interface{}` with `any` Signed-off-by: Thomas Hipp * pkg/crypto: Replace `interface{}` with `any` Signed-off-by: Thomas Hipp * pkg/http: Replace `interface{}` with `any` Signed-off-by: Thomas Hipp * pkg/oidc: Replace `interface{}` with `any` Signed-off-by: Thomas Hipp * pkg/op: Replace `interface{}` with `any` Signed-off-by: Thomas Hipp --------- Signed-off-by: Thomas Hipp --- example/client/service/service.go | 4 ++-- example/server/storage/storage.go | 24 +++++++++++------------ example/server/storage/storage_dynamic.go | 2 +- pkg/client/client.go | 10 +++++----- pkg/client/rs/resource_server.go | 12 ++++++------ pkg/client/rs/resource_server_test.go | 4 ++-- pkg/client/tokenexchange/tokenexchange.go | 10 +++++----- pkg/crypto/sign.go | 2 +- pkg/http/http.go | 10 +++++----- pkg/http/marshal.go | 4 ++-- pkg/http/marshal_test.go | 2 +- pkg/oidc/error.go | 2 +- pkg/oidc/keyset.go | 6 +++--- pkg/oidc/regression_test.go | 6 +++--- pkg/oidc/token.go | 6 +++--- pkg/oidc/token_request.go | 4 ++-- pkg/oidc/token_test.go | 12 ++++++------ pkg/oidc/types.go | 6 +++--- pkg/oidc/verifier.go | 2 +- pkg/op/auth_request.go | 2 +- pkg/op/auth_request_test.go | 6 +++--- pkg/op/signer.go | 4 ++-- pkg/op/storage.go | 8 ++++---- pkg/op/token.go | 2 +- pkg/op/token_exchange.go | 16 +++++++-------- 25 files changed, 83 insertions(+), 83 deletions(-) diff --git a/example/client/service/service.go b/example/client/service/service.go index 9526174..7d91f31 100644 --- a/example/client/service/service.go +++ b/example/client/service/service.go @@ -125,7 +125,7 @@ func main() { testURL := r.Form.Get("url") var data struct { URL string - Response interface{} + Response any } if testURL != "" { data.URL = testURL @@ -149,7 +149,7 @@ func main() { logrus.Fatal(http.ListenAndServe("127.0.0.1:"+port, nil)) } -func callExampleEndpoint(client *http.Client, testURL string) (interface{}, error) { +func callExampleEndpoint(client *http.Client, testURL string) (any, error) { req, err := http.NewRequest("GET", testURL, nil) if err != nil { return nil, err diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index 406300b..3015626 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -61,7 +61,7 @@ func (s *signingKey) SignatureAlgorithm() jose.SignatureAlgorithm { return s.algorithm } -func (s *signingKey) Key() interface{} { +func (s *signingKey) Key() any { return s.key } @@ -85,7 +85,7 @@ func (s *publicKey) Use() string { return "sig" } -func (s *publicKey) Key() interface{} { +func (s *publicKey) Key() any { return &s.key.PublicKey } @@ -525,11 +525,11 @@ func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection * // GetPrivateClaimsFromScopes implements the op.Storage interface // it will be called for the creation of a JWT access token to assert claims for custom scopes -func (s *Storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) { +func (s *Storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]any, err error) { return s.getPrivateClaimsFromScopes(ctx, userID, clientID, scopes) } -func (s *Storage) getPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) { +func (s *Storage) getPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]any, err error) { for _, scope := range scopes { switch scope { case CustomScope: @@ -713,7 +713,7 @@ func (s *Storage) CreateTokenExchangeRequest(ctx context.Context, request op.Tok // GetPrivateClaimsFromScopesForTokenExchange implements the op.TokenExchangeStorage interface // it will be called for the creation of an exchanged JWT access token to assert claims for custom scopes // plus adding token exchange specific claims related to delegation or impersonation -func (s *Storage) GetPrivateClaimsFromTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]interface{}, err error) { +func (s *Storage) GetPrivateClaimsFromTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]any, err error) { claims, err = s.getPrivateClaimsFromScopes(ctx, "", request.GetClientID(), request.GetScopes()) if err != nil { return nil, err @@ -742,12 +742,12 @@ func (s *Storage) SetUserinfoFromTokenExchangeRequest(ctx context.Context, useri return nil } -func (s *Storage) getTokenExchangeClaims(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]interface{}) { +func (s *Storage) getTokenExchangeClaims(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]any) { for _, scope := range request.GetScopes() { switch { case strings.HasPrefix(scope, CustomScopeImpersonatePrefix) && request.GetExchangeActor() == "": // Set actor subject claim for impersonation flow - claims = appendClaim(claims, "act", map[string]interface{}{ + claims = appendClaim(claims, "act", map[string]any{ "sub": request.GetExchangeSubject(), }) } @@ -755,7 +755,7 @@ func (s *Storage) getTokenExchangeClaims(ctx context.Context, request op.TokenEx // Set actor subject claim for delegation flow // if request.GetExchangeActor() != "" { - // claims = appendClaim(claims, "act", map[string]interface{}{ + // claims = appendClaim(claims, "act", map[string]any{ // "sub": request.GetExchangeActor(), // }) // } @@ -777,16 +777,16 @@ func getInfoFromRequest(req op.TokenRequest) (clientID string, authTime time.Tim } // customClaim demonstrates how to return custom claims based on provided information -func customClaim(clientID string) map[string]interface{} { - return map[string]interface{}{ +func customClaim(clientID string) map[string]any { + return map[string]any{ "client": clientID, "other": "stuff", } } -func appendClaim(claims map[string]interface{}, claim string, value interface{}) map[string]interface{} { +func appendClaim(claims map[string]any, claim string, value any) map[string]any { if claims == nil { - claims = make(map[string]interface{}) + claims = make(map[string]any) } claims[claim] = value return claims diff --git a/example/server/storage/storage_dynamic.go b/example/server/storage/storage_dynamic.go index 07af903..cb16c02 100644 --- a/example/server/storage/storage_dynamic.go +++ b/example/server/storage/storage_dynamic.go @@ -239,7 +239,7 @@ func (s *multiStorage) SetIntrospectionFromToken(ctx context.Context, introspect // GetPrivateClaimsFromScopes implements the op.Storage interface // it will be called for the creation of a JWT access token to assert claims for custom scopes -func (s *multiStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) { +func (s *multiStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]any, err error) { storage, err := s.storageFromContext(ctx) if err != nil { return nil, err diff --git a/pkg/client/client.go b/pkg/client/client.go index f6a407b..7486ef1 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -48,11 +48,11 @@ type TokenEndpointCaller interface { HttpClient() *http.Client } -func CallTokenEndpoint(request interface{}, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) { +func CallTokenEndpoint(request any, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) { return callTokenEndpoint(request, nil, caller) } -func callTokenEndpoint(request interface{}, authFn interface{}, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) { +func callTokenEndpoint(request any, authFn any, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) { req, err := httphelper.FormRequest(caller.TokenEndpoint(), request, Encoder, authFn) if err != nil { return nil, err @@ -80,7 +80,7 @@ type EndSessionCaller interface { HttpClient() *http.Client } -func CallEndSessionEndpoint(request interface{}, authFn interface{}, caller EndSessionCaller) (*url.URL, error) { +func CallEndSessionEndpoint(request any, authFn any, caller EndSessionCaller) (*url.URL, error) { req, err := httphelper.FormRequest(caller.GetEndSessionEndpoint(), request, Encoder, authFn) if err != nil { return nil, err @@ -123,7 +123,7 @@ type RevokeRequest struct { ClientSecret string `schema:"client_secret"` } -func CallRevokeEndpoint(request interface{}, authFn interface{}, caller RevokeCaller) error { +func CallRevokeEndpoint(request any, authFn any, caller RevokeCaller) error { req, err := httphelper.FormRequest(caller.GetRevokeEndpoint(), request, Encoder, authFn) if err != nil { return err @@ -151,7 +151,7 @@ func CallRevokeEndpoint(request interface{}, authFn interface{}, caller RevokeCa return nil } -func CallTokenExchangeEndpoint(request interface{}, authFn interface{}, caller TokenEndpointCaller) (resp *oidc.TokenExchangeResponse, err error) { +func CallTokenExchangeEndpoint(request any, authFn any, caller TokenEndpointCaller) (resp *oidc.TokenExchangeResponse, err error) { req, err := httphelper.FormRequest(caller.TokenEndpoint(), request, Encoder, authFn) if err != nil { return nil, err diff --git a/pkg/client/rs/resource_server.go b/pkg/client/rs/resource_server.go index c641940..95b6e2e 100644 --- a/pkg/client/rs/resource_server.go +++ b/pkg/client/rs/resource_server.go @@ -15,7 +15,7 @@ type ResourceServer interface { IntrospectionURL() string TokenEndpoint() string HttpClient() *http.Client - AuthFn() (interface{}, error) + AuthFn() (any, error) } type resourceServer struct { @@ -23,7 +23,7 @@ type resourceServer struct { tokenURL string introspectURL string httpClient *http.Client - authFn func() (interface{}, error) + authFn func() (any, error) } func (r *resourceServer) IntrospectionURL() string { @@ -38,12 +38,12 @@ func (r *resourceServer) HttpClient() *http.Client { return r.httpClient } -func (r *resourceServer) AuthFn() (interface{}, error) { +func (r *resourceServer) AuthFn() (any, error) { return r.authFn() } func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option ...Option) (ResourceServer, error) { - authorizer := func() (interface{}, error) { + authorizer := func() (any, error) { return httphelper.AuthorizeBasic(clientID, clientSecret), nil } return newResourceServer(issuer, authorizer, option...) @@ -54,7 +54,7 @@ func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, opt if err != nil { return nil, err } - authorizer := func() (interface{}, error) { + authorizer := func() (any, error) { assertion, err := client.SignedJWTProfileAssertion(clientID, []string{issuer}, time.Hour, signer) if err != nil { return nil, err @@ -64,7 +64,7 @@ func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, opt return newResourceServer(issuer, authorizer, options...) } -func newResourceServer(issuer string, authorizer func() (interface{}, error), options ...Option) (*resourceServer, error) { +func newResourceServer(issuer string, authorizer func() (any, error), options ...Option) (*resourceServer, error) { rs := &resourceServer{ issuer: issuer, httpClient: httphelper.DefaultHTTPClient, diff --git a/pkg/client/rs/resource_server_test.go b/pkg/client/rs/resource_server_test.go index b5fb496..16cb6ad 100644 --- a/pkg/client/rs/resource_server_test.go +++ b/pkg/client/rs/resource_server_test.go @@ -11,14 +11,14 @@ import ( func TestNewResourceServer(t *testing.T) { type args struct { issuer string - authorizer func() (interface{}, error) + authorizer func() (any, error) options []Option } type wantFields struct { issuer string tokenURL string introspectURL string - authFn func() (interface{}, error) + authFn func() (any, error) } tests := []struct { name string diff --git a/pkg/client/tokenexchange/tokenexchange.go b/pkg/client/tokenexchange/tokenexchange.go index 1375f68..4ae5507 100644 --- a/pkg/client/tokenexchange/tokenexchange.go +++ b/pkg/client/tokenexchange/tokenexchange.go @@ -12,13 +12,13 @@ import ( type TokenExchanger interface { TokenEndpoint() string HttpClient() *http.Client - AuthFn() (interface{}, error) + AuthFn() (any, error) } type OAuthTokenExchange struct { httpClient *http.Client tokenEndpoint string - authFn func() (interface{}, error) + authFn func() (any, error) } func NewTokenExchanger(issuer string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) { @@ -26,13 +26,13 @@ func NewTokenExchanger(issuer string, options ...func(source *OAuthTokenExchange } func NewTokenExchangerClientCredentials(issuer, clientID, clientSecret string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) { - authorizer := func() (interface{}, error) { + authorizer := func() (any, error) { return httphelper.AuthorizeBasic(clientID, clientSecret), nil } return newOAuthTokenExchange(issuer, authorizer, options...) } -func newOAuthTokenExchange(issuer string, authorizer func() (interface{}, error), options ...func(source *OAuthTokenExchange)) (*OAuthTokenExchange, error) { +func newOAuthTokenExchange(issuer string, authorizer func() (any, error), options ...func(source *OAuthTokenExchange)) (*OAuthTokenExchange, error) { te := &OAuthTokenExchange{ httpClient: httphelper.DefaultHTTPClient, } @@ -78,7 +78,7 @@ func (te *OAuthTokenExchange) HttpClient() *http.Client { return te.httpClient } -func (te *OAuthTokenExchange) AuthFn() (interface{}, error) { +func (te *OAuthTokenExchange) AuthFn() (any, error) { if te.authFn != nil { return te.authFn() } diff --git a/pkg/crypto/sign.go b/pkg/crypto/sign.go index a0b9cae..90e4c0e 100644 --- a/pkg/crypto/sign.go +++ b/pkg/crypto/sign.go @@ -7,7 +7,7 @@ import ( "gopkg.in/square/go-jose.v2" ) -func Sign(object interface{}, signer jose.Signer) (string, error) { +func Sign(object any, signer jose.Signer) (string, error) { payload, err := json.Marshal(object) if err != nil { return "", err diff --git a/pkg/http/http.go b/pkg/http/http.go index d3c5b4f..46f8250 100644 --- a/pkg/http/http.go +++ b/pkg/http/http.go @@ -17,11 +17,11 @@ var DefaultHTTPClient = &http.Client{ } type Decoder interface { - Decode(dst interface{}, src map[string][]string) error + Decode(dst any, src map[string][]string) error } type Encoder interface { - Encode(src interface{}, dst map[string][]string) error + Encode(src any, dst map[string][]string) error } type FormAuthorization func(url.Values) @@ -33,7 +33,7 @@ func AuthorizeBasic(user, password string) RequestAuthorization { } } -func FormRequest(endpoint string, request interface{}, encoder Encoder, authFn interface{}) (*http.Request, error) { +func FormRequest(endpoint string, request any, encoder Encoder, authFn any) (*http.Request, error) { form := url.Values{} if err := encoder.Encode(request, form); err != nil { return nil, err @@ -53,7 +53,7 @@ func FormRequest(endpoint string, request interface{}, encoder Encoder, authFn i return req, nil } -func HttpRequest(client *http.Client, req *http.Request, response interface{}) error { +func HttpRequest(client *http.Client, req *http.Request, response any) error { resp, err := client.Do(req) if err != nil { return err @@ -76,7 +76,7 @@ func HttpRequest(client *http.Client, req *http.Request, response interface{}) e return nil } -func URLEncodeParams(resp interface{}, encoder Encoder) (url.Values, error) { +func URLEncodeParams(resp any, encoder Encoder) (url.Values, error) { values := make(map[string][]string) err := encoder.Encode(resp, values) if err != nil { diff --git a/pkg/http/marshal.go b/pkg/http/marshal.go index 794a28a..71ed2c2 100644 --- a/pkg/http/marshal.go +++ b/pkg/http/marshal.go @@ -8,11 +8,11 @@ import ( "reflect" ) -func MarshalJSON(w http.ResponseWriter, i interface{}) { +func MarshalJSON(w http.ResponseWriter, i any) { MarshalJSONWithStatus(w, i, http.StatusOK) } -func MarshalJSONWithStatus(w http.ResponseWriter, i interface{}, status int) { +func MarshalJSONWithStatus(w http.ResponseWriter, i any, status int) { w.Header().Set("content-type", "application/json") w.WriteHeader(status) if i == nil || (reflect.ValueOf(i).Kind() == reflect.Ptr && reflect.ValueOf(i).IsNil()) { diff --git a/pkg/http/marshal_test.go b/pkg/http/marshal_test.go index 3838a44..dcc7fdd 100644 --- a/pkg/http/marshal_test.go +++ b/pkg/http/marshal_test.go @@ -94,7 +94,7 @@ func TestConcatenateJSON(t *testing.T) { func TestMarshalJSONWithStatus(t *testing.T) { type args struct { - i interface{} + i any status int } type res struct { diff --git a/pkg/oidc/error.go b/pkg/oidc/error.go index 79acecd..9e265b3 100644 --- a/pkg/oidc/error.go +++ b/pkg/oidc/error.go @@ -151,7 +151,7 @@ func (e *Error) WithParent(err error) *Error { return e } -func (e *Error) WithDescription(desc string, args ...interface{}) *Error { +func (e *Error) WithDescription(desc string, args ...any) *Error { e.Description = fmt.Sprintf(desc, args...) return e } diff --git a/pkg/oidc/keyset.go b/pkg/oidc/keyset.go index c6e865b..7b766a5 100644 --- a/pkg/oidc/keyset.go +++ b/pkg/oidc/keyset.go @@ -46,8 +46,8 @@ func GetKeyIDAndAlg(jws *jose.JSONWebSignature) (string, string) { // // will return false none or multiple match // -//deprecated: use FindMatchingKey which will return an error (more specific) instead of just a bool -//moved implementation already to FindMatchingKey +// deprecated: use FindMatchingKey which will return an error (more specific) instead of just a bool +// moved implementation already to FindMatchingKey func FindKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (jose.JSONWebKey, bool) { key, err := FindMatchingKey(keyID, use, expectedAlg, keys...) return key, err == nil @@ -91,7 +91,7 @@ func FindMatchingKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (k return key, ErrKeyNone } -func algToKeyType(key interface{}, alg string) bool { +func algToKeyType(key any, alg string) bool { switch alg[0] { case 'R', 'P': _, ok := key.(*rsa.PublicKey) diff --git a/pkg/oidc/regression_test.go b/pkg/oidc/regression_test.go index 5d33bb6..9cb3ff9 100644 --- a/pkg/oidc/regression_test.go +++ b/pkg/oidc/regression_test.go @@ -17,7 +17,7 @@ const dataDir = "regression_data" // jsonFilename builds a filename for the regression testdata. // dataDir/.json -func jsonFilename(obj interface{}) string { +func jsonFilename(obj any) string { name := fmt.Sprintf("%T.json", obj) return path.Join( dataDir, @@ -25,13 +25,13 @@ func jsonFilename(obj interface{}) string { ) } -func encodeJSON(t *testing.T, w io.Writer, obj interface{}) { +func encodeJSON(t *testing.T, w io.Writer, obj any) { enc := json.NewEncoder(w) enc.SetIndent("", "\t") require.NoError(t, enc.Encode(obj)) } -var regressionData = []interface{}{ +var regressionData = []any{ accessTokenData, idTokenData, introspectionResponseData, diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index 5283eb5..36d546c 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -222,7 +222,7 @@ type JWTProfileAssertionClaims struct { Expiration Time `json:"exp"` IssuedAt Time `json:"iat"` - Claims map[string]interface{} `json:"-"` + Claims map[string]any `json:"-"` } type jpaAlias JWTProfileAssertionClaims @@ -262,7 +262,7 @@ func JWTProfileDelegatedSubject(sub string) func(*JWTProfileAssertionClaims) { } } -func JWTProfileCustomClaim(key string, value interface{}) func(*JWTProfileAssertionClaims) { +func JWTProfileCustomClaim(key string, value any) func(*JWTProfileAssertionClaims) { return func(j *JWTProfileAssertionClaims) { j.Claims[key] = value } @@ -292,7 +292,7 @@ func NewJWTProfileAssertion(userID, keyID string, audience []string, key []byte, IssuedAt: FromTime(time.Now().UTC()), Expiration: FromTime(time.Now().Add(1 * time.Hour).UTC()), Audience: audience, - Claims: make(map[string]interface{}), + Claims: make(map[string]any), } for _, opt := range opts { diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index 5c5cf20..07c4ca0 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -130,7 +130,7 @@ type JWTTokenRequest struct { IssuedAt Time `json:"iat"` ExpiresAt Time `json:"exp"` - private map[string]interface{} + private map[string]any } func (j *JWTTokenRequest) MarshalJSON() ([]byte, error) { @@ -171,7 +171,7 @@ func (j *JWTTokenRequest) UnmarshalJSON(data []byte) error { return nil } -func (j *JWTTokenRequest) GetCustomClaim(key string) interface{} { +func (j *JWTTokenRequest) GetCustomClaim(key string) any { return j.private[key] } diff --git a/pkg/oidc/token_test.go b/pkg/oidc/token_test.go index ef1e77f..f3ea8d2 100644 --- a/pkg/oidc/token_test.go +++ b/pkg/oidc/token_test.go @@ -29,7 +29,7 @@ var ( accessTokenData = &AccessTokenClaims{ TokenClaims: tokenClaimsData, Scopes: []string{"email", "phone"}, - Claims: map[string]interface{}{ + Claims: map[string]any{ "foo": "bar", }, } @@ -43,7 +43,7 @@ var ( UserInfoEmail: userInfoData.UserInfoEmail, UserInfoPhone: userInfoData.UserInfoPhone, Address: userInfoData.Address, - Claims: map[string]interface{}{ + Claims: map[string]any{ "foo": "bar", }, } @@ -64,7 +64,7 @@ var ( UserInfoEmail: userInfoData.UserInfoEmail, UserInfoPhone: userInfoData.UserInfoPhone, Address: userInfoData.Address, - Claims: map[string]interface{}{ + Claims: map[string]any{ "foo": "bar", }, } @@ -102,7 +102,7 @@ var ( PostalCode: "666-666", Country: "Moon", }, - Claims: map[string]interface{}{ + Claims: map[string]any{ "foo": "bar", }, } @@ -114,7 +114,7 @@ var ( Audience: Audience{"foo", "bar"}, Expiration: 12345, IssuedAt: 12000, - Claims: map[string]interface{}{ + Claims: map[string]any{ "foo": "bar", }, } @@ -181,7 +181,7 @@ func TestIDTokenClaims_SetUserInfo(t *testing.T) { UserInfoEmail: userInfoData.UserInfoEmail, UserInfoPhone: userInfoData.UserInfoPhone, Address: userInfoData.Address, - Claims: map[string]interface{}{ + Claims: map[string]any{ "foo": "bar", }, } diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index 23367ef..6ab7469 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -17,13 +17,13 @@ import ( type Audience []string func (a *Audience) UnmarshalJSON(text []byte) error { - var i interface{} + var i any err := json.Unmarshal(text, &i) if err != nil { return err } switch aud := i.(type) { - case []interface{}: + case []any: *a = make([]string, len(aud)) for i, audience := range aud { (*a)[i] = audience.(string) @@ -177,7 +177,7 @@ func (s *SpaceDelimitedArray) UnmarshalJSON(data []byte) error { return nil } -func (s *SpaceDelimitedArray) Scan(src interface{}) error { +func (s *SpaceDelimitedArray) Scan(src any) error { if src == nil { *s = nil return nil diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go index c4ee95e..1af1ebb 100644 --- a/pkg/oidc/verifier.go +++ b/pkg/oidc/verifier.go @@ -85,7 +85,7 @@ func DecryptToken(tokenString string) (string, error) { return tokenString, nil // TODO: impl } -func ParseToken(tokenString string, claims interface{}) ([]byte, error) { +func ParseToken(tokenString string, claims any) ([]byte, error) { parts := strings.Split(tokenString, ".") if len(parts) != 3 { return nil, fmt.Errorf("%w: token contains an invalid number of segments", ErrParse) diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 5845756..7d9f264 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -501,7 +501,7 @@ func BuildAuthRequestCode(authReq AuthRequest, crypto Crypto) (string, error) { // AuthResponseURL encodes the authorization response (successful and error) and sets it as query or fragment values // depending on the response_mode and response_type -func AuthResponseURL(redirectURI string, responseType oidc.ResponseType, responseMode oidc.ResponseMode, response interface{}, encoder httphelper.Encoder) (string, error) { +func AuthResponseURL(redirectURI string, responseType oidc.ResponseType, responseMode oidc.ResponseMode, response any, encoder httphelper.Encoder) (string, error) { uri, err := url.Parse(redirectURI) if err != nil { return "", oidc.ErrServerError().WithParent(err) diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index 1fadffc..e8c9085 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -745,7 +745,7 @@ func TestAuthResponseURL(t *testing.T) { redirectURI string responseType oidc.ResponseType responseMode oidc.ResponseMode - response interface{} + response any encoder httphelper.Encoder } type res struct { @@ -763,7 +763,7 @@ func TestAuthResponseURL(t *testing.T) { "uri", oidc.ResponseTypeCode, "", - map[string]interface{}{"test": "test"}, + map[string]any{"test": "test"}, &mockEncoder{ errors.New("error encoding"), }, @@ -934,7 +934,7 @@ type mockEncoder struct { err error } -func (m *mockEncoder) Encode(src interface{}, dst map[string][]string) error { +func (m *mockEncoder) Encode(src any, dst map[string][]string) error { if m.err != nil { return m.err } diff --git a/pkg/op/signer.go b/pkg/op/signer.go index 7e488f6..6cef288 100644 --- a/pkg/op/signer.go +++ b/pkg/op/signer.go @@ -10,7 +10,7 @@ var ErrSignerCreationFailed = errors.New("signer creation failed") type SigningKey interface { SignatureAlgorithm() jose.SignatureAlgorithm - Key() interface{} + Key() any ID() string } @@ -32,5 +32,5 @@ type Key interface { ID() string Algorithm() jose.SignatureAlgorithm Use() string - Key() interface{} + Key() any } diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 72b75e0..17aa0b4 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -100,7 +100,7 @@ type TokenExchangeStorage interface { // GetPrivateClaimsFromTokenExchangeRequest will be called during access token creation. // Claims evaluation can be based on all validated request data available, including: scopes, resource, audience, etc. - GetPrivateClaimsFromTokenExchangeRequest(ctx context.Context, request TokenExchangeRequest) (claims map[string]interface{}, err error) + GetPrivateClaimsFromTokenExchangeRequest(ctx context.Context, request TokenExchangeRequest) (claims map[string]any, err error) // SetUserinfoFromTokenExchangeRequest will be called during id token creation. // Claims evaluation can be based on all validated request data available, including: scopes, resource, audience, etc. @@ -110,8 +110,8 @@ type TokenExchangeStorage interface { // TokenExchangeTokensVerifierStorage is an optional interface used in token exchange process to verify tokens // issued by third-party applications. If interface is not implemented - only tokens issued by op will be exchanged. type TokenExchangeTokensVerifierStorage interface { - VerifyExchangeSubjectToken(ctx context.Context, token string, tokenType oidc.TokenType) (tokenIDOrToken string, subject string, tokenClaims map[string]interface{}, err error) - VerifyExchangeActorToken(ctx context.Context, token string, tokenType oidc.TokenType) (tokenIDOrToken string, actor string, tokenClaims map[string]interface{}, err error) + VerifyExchangeSubjectToken(ctx context.Context, token string, tokenType oidc.TokenType) (tokenIDOrToken string, subject string, tokenClaims map[string]any, err error) + VerifyExchangeActorToken(ctx context.Context, token string, tokenType oidc.TokenType) (tokenIDOrToken string, actor string, tokenClaims map[string]any, err error) } var ErrInvalidRefreshToken = errors.New("invalid_refresh_token") @@ -126,7 +126,7 @@ type OPStorage interface { SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, subject, origin string) error SetIntrospectionFromToken(ctx context.Context, userinfo *oidc.IntrospectionResponse, tokenID, subject, clientID string) error - GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) + GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]any, error) GetKeyByIDAndClientID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error) ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error) } diff --git a/pkg/op/token.go b/pkg/op/token.go index ae82b06..001023c 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -122,7 +122,7 @@ func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, ex restrictedScopes := client.RestrictAdditionalAccessTokenScopes()(tokenRequest.GetScopes()) var ( - privateClaims map[string]interface{} + privateClaims map[string]any err error ) diff --git a/pkg/op/token_exchange.go b/pkg/op/token_exchange.go index 4f1ed43..e64ce80 100644 --- a/pkg/op/token_exchange.go +++ b/pkg/op/token_exchange.go @@ -24,12 +24,12 @@ type TokenExchangeRequest interface { GetExchangeSubject() string GetExchangeSubjectTokenType() oidc.TokenType GetExchangeSubjectTokenIDOrToken() string - GetExchangeSubjectTokenClaims() map[string]interface{} + GetExchangeSubjectTokenClaims() map[string]any GetExchangeActor() string GetExchangeActorTokenType() oidc.TokenType GetExchangeActorTokenIDOrToken() string - GetExchangeActorTokenClaims() map[string]interface{} + GetExchangeActorTokenClaims() map[string]any SetCurrentScopes(scopes []string) SetRequestedTokenType(tt oidc.TokenType) @@ -40,12 +40,12 @@ type tokenExchangeRequest struct { exchangeSubjectTokenIDOrToken string exchangeSubjectTokenType oidc.TokenType exchangeSubject string - exchangeSubjectTokenClaims map[string]interface{} + exchangeSubjectTokenClaims map[string]any exchangeActorTokenIDOrToken string exchangeActorTokenType oidc.TokenType exchangeActor string - exchangeActorTokenClaims map[string]interface{} + exchangeActorTokenClaims map[string]any resource []string audience oidc.Audience @@ -96,7 +96,7 @@ func (r *tokenExchangeRequest) GetExchangeSubjectTokenIDOrToken() string { return r.exchangeSubjectTokenIDOrToken } -func (r *tokenExchangeRequest) GetExchangeSubjectTokenClaims() map[string]interface{} { +func (r *tokenExchangeRequest) GetExchangeSubjectTokenClaims() map[string]any { return r.exchangeSubjectTokenClaims } @@ -112,7 +112,7 @@ func (r *tokenExchangeRequest) GetExchangeActorTokenIDOrToken() string { return r.exchangeActorTokenIDOrToken } -func (r *tokenExchangeRequest) GetExchangeActorTokenClaims() map[string]interface{} { +func (r *tokenExchangeRequest) GetExchangeActorTokenClaims() map[string]any { return r.exchangeActorTokenClaims } @@ -232,7 +232,7 @@ func ValidateTokenExchangeRequest( var ( exchangeActorTokenIDOrToken, exchangeActor string - exchangeActorTokenClaims map[string]interface{} + exchangeActorTokenClaims map[string]any ) if oidcTokenExchangeRequest.ActorToken != "" { exchangeActorTokenIDOrToken, exchangeActor, exchangeActorTokenClaims, ok = GetTokenIDAndSubjectFromToken(ctx, exchanger, @@ -281,7 +281,7 @@ func GetTokenIDAndSubjectFromToken( token string, tokenType oidc.TokenType, isActor bool, -) (tokenIDOrToken, subject string, claims map[string]interface{}, ok bool) { +) (tokenIDOrToken, subject string, claims map[string]any, ok bool) { switch tokenType { case oidc.AccessTokenType: var accessTokenClaims *oidc.AccessTokenClaims From 1291bf6881e8642a1059d55a8252ae20e7a23f85 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Oct 2023 12:41:50 +0300 Subject: [PATCH 289/502] chore(deps): bump github.com/rs/cors from 1.10.0 to 1.10.1 (#451) Bumps [github.com/rs/cors](https://github.com/rs/cors) from 1.10.0 to 1.10.1. - [Release notes](https://github.com/rs/cors/releases) - [Commits](https://github.com/rs/cors/compare/v1.10.0...v1.10.1) --- updated-dependencies: - dependency-name: github.com/rs/cors dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3f75819..5a05848 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/jeremija/gosubmit v0.2.7 github.com/muhlemmer/gu v0.3.1 github.com/muhlemmer/httpforwarded v0.1.0 - github.com/rs/cors v1.10.0 + github.com/rs/cors v1.10.1 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 go.opentelemetry.io/otel v1.19.0 diff --git a/go.sum b/go.sum index 89cea25..fdb3c35 100644 --- a/go.sum +++ b/go.sum @@ -39,8 +39,8 @@ github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/ github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rs/cors v1.10.0 h1:62NOS1h+r8p1mW6FM0FSB0exioXLhd/sh15KpjWBZ+8= -github.com/rs/cors v1.10.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= +github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From bb115d8f6a68bdb7cb42fb861499fb63a316bd06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Oct 2023 12:44:59 +0300 Subject: [PATCH 290/502] chore(deps): bump golang.org/x/oauth2 from 0.12.0 to 0.13.0 (#454) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.12.0 to 0.13.0. - [Commits](https://github.com/golang/oauth2/compare/v0.12.0...v0.13.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5a05848..6cdd591 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/stretchr/testify v1.8.4 go.opentelemetry.io/otel v1.19.0 go.opentelemetry.io/otel/trace v1.19.0 - golang.org/x/oauth2 v0.12.0 + golang.org/x/oauth2 v0.13.0 golang.org/x/text v0.13.0 gopkg.in/square/go-jose.v2 v2.6.0 ) diff --git a/go.sum b/go.sum index fdb3c35..a3b2001 100644 --- a/go.sum +++ b/go.sum @@ -68,8 +68,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= -golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= From 0dc2a6e7a1bb31e1a470527a4ac95b5e98286f61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 13 Oct 2023 15:17:03 +0300 Subject: [PATCH 291/502] fix(op): return state in token response only for implicit flow (#460) * fix(op): return state in token response only for implicit flow * oops --- pkg/op/token.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/op/token.go b/pkg/op/token.go index bc45c29..63a01a6 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -51,7 +51,10 @@ func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Cli if err != nil { return nil, err } - state = authRequest.GetState() + // only implicit flow requires state to be returned. + if code == "" { + state = authRequest.GetState() + } } exp := uint64(validity.Seconds()) From 434b2e62d8f2703a86b95a4dacc9647777aad491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Mon, 16 Oct 2023 12:02:56 +0300 Subject: [PATCH 292/502] chore(op): upgrade go-chi/chi to v5 (#462) --- example/client/api/api.go | 2 +- example/server/dynamic/login.go | 2 +- example/server/dynamic/op.go | 2 +- example/server/exampleop/device.go | 2 +- example/server/exampleop/login.go | 2 +- example/server/exampleop/op.go | 2 +- go.mod | 2 +- go.sum | 4 ++-- pkg/op/op.go | 2 +- pkg/op/server_http.go | 2 +- pkg/op/server_legacy.go | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/example/client/api/api.go b/example/client/api/api.go index 2f81c07..2e61c21 100644 --- a/example/client/api/api.go +++ b/example/client/api/api.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/go-chi/chi" + "github.com/go-chi/chi/v5" "github.com/sirupsen/logrus" "github.com/zitadel/oidc/v3/pkg/client/rs" diff --git a/example/server/dynamic/login.go b/example/server/dynamic/login.go index d90fb8e..685b444 100644 --- a/example/server/dynamic/login.go +++ b/example/server/dynamic/login.go @@ -6,7 +6,7 @@ import ( "html/template" "net/http" - "github.com/go-chi/chi" + "github.com/go-chi/chi/v5" "github.com/zitadel/oidc/v3/pkg/op" ) diff --git a/example/server/dynamic/op.go b/example/server/dynamic/op.go index 1662729..432a575 100644 --- a/example/server/dynamic/op.go +++ b/example/server/dynamic/op.go @@ -7,7 +7,7 @@ import ( "log" "net/http" - "github.com/go-chi/chi" + "github.com/go-chi/chi/v5" "golang.org/x/text/language" "github.com/zitadel/oidc/v3/example/server/storage" diff --git a/example/server/exampleop/device.go b/example/server/exampleop/device.go index 7478750..2f9be52 100644 --- a/example/server/exampleop/device.go +++ b/example/server/exampleop/device.go @@ -8,7 +8,7 @@ import ( "net/http" "net/url" - "github.com/go-chi/chi" + "github.com/go-chi/chi/v5" "github.com/gorilla/securecookie" "github.com/sirupsen/logrus" "github.com/zitadel/oidc/v3/pkg/op" diff --git a/example/server/exampleop/login.go b/example/server/exampleop/login.go index 053525d..4d2b478 100644 --- a/example/server/exampleop/login.go +++ b/example/server/exampleop/login.go @@ -5,7 +5,7 @@ import ( "fmt" "net/http" - "github.com/go-chi/chi" + "github.com/go-chi/chi/v5" "github.com/zitadel/oidc/v3/pkg/op" ) diff --git a/example/server/exampleop/op.go b/example/server/exampleop/op.go index 830f7f6..74018da 100644 --- a/example/server/exampleop/op.go +++ b/example/server/exampleop/op.go @@ -7,7 +7,7 @@ import ( "sync/atomic" "time" - "github.com/go-chi/chi" + "github.com/go-chi/chi/v5" "github.com/zitadel/logging" "golang.org/x/exp/slog" "golang.org/x/text/language" diff --git a/go.mod b/go.mod index fee739b..d3245eb 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/zitadel/oidc/v3 go 1.19 require ( - github.com/go-chi/chi v1.5.4 + github.com/go-chi/chi/v5 v5.0.10 github.com/go-jose/go-jose/v3 v3.0.0 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 diff --git a/go.sum b/go.sum index e1acdae..c57f8da 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ 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/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= -github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= +github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= +github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= diff --git a/pkg/op/op.go b/pkg/op/op.go index 5abe08f..2bd130b 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -6,7 +6,7 @@ import ( "net/http" "time" - "github.com/go-chi/chi" + "github.com/go-chi/chi/v5" jose "github.com/go-jose/go-jose/v3" "github.com/rs/cors" "github.com/zitadel/schema" diff --git a/pkg/op/server_http.go b/pkg/op/server_http.go index 3fb481d..96ee7a5 100644 --- a/pkg/op/server_http.go +++ b/pkg/op/server_http.go @@ -5,7 +5,7 @@ import ( "net/http" "net/url" - "github.com/go-chi/chi" + "github.com/go-chi/chi/v5" "github.com/rs/cors" "github.com/zitadel/logging" httphelper "github.com/zitadel/oidc/v3/pkg/http" diff --git a/pkg/op/server_legacy.go b/pkg/op/server_legacy.go index 0a7de85..5907e28 100644 --- a/pkg/op/server_legacy.go +++ b/pkg/op/server_legacy.go @@ -6,7 +6,7 @@ import ( "net/http" "time" - "github.com/go-chi/chi" + "github.com/go-chi/chi/v5" "github.com/zitadel/oidc/v3/pkg/oidc" ) From 9c0696306f3b870832695ee048be00ab89543957 Mon Sep 17 00:00:00 2001 From: mffap Date: Mon, 23 Oct 2023 16:16:48 +0200 Subject: [PATCH 293/502] docs: update security policy (#464) --- SECURITY.md | 34 ++-------------------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index d682630..ec216f2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ # Security Policy -At ZITADEL we are extremely grateful for security aware people that disclose vulnerabilities to us and the open source community. All reports will be investigated by our team. +Please refer to the security policy [on zitadel/zitadel](https://github.com/zitadel/zitadel/blob/main/SECURITY.md) which is applicable for all open source repositories of our organization. ## Supported Versions @@ -18,34 +18,4 @@ We currently support the following version of the OIDC framework: [2]: https://github.com/zitadel/oidc/discussions/378 [3]: https://github.com/zitadel/oidc/tree/main [4]: https://github.com/zitadel/oidc/tree/next -[5]: https://github.com/zitadel/oidc/milestone/2 - -## Reporting a vulnerability - -To file a incident, please disclose by email to security@zitadel.com with the security details. - -At the moment GPG encryption is no yet supported, however you may sign your message at will. - -### When should I report a vulnerability - -* You think you discovered a ... - * ... potential security vulnerability in the SDK - * ... vulnerability in another project that this SDK bases on -* For projects with their own vulnerability reporting and disclosure process, please report it directly there - -### When should I NOT report a vulnerability - -* You need help applying security related updates -* Your issue is not security related - -## Security Vulnerability Response - -TBD - -## Public Disclosure - -All accepted and mitigated vulnerabilities will be published on the [Github Security Page](https://github.com/zitadel/oidc/security/advisories) - -### Timing - -We think it is crucial to publish advisories `ASAP` as mitigations are ready. But due to the unknown nature of the disclosures the time frame can range from 7 to 90 days. +[5]: https://github.com/zitadel/oidc/milestone/2 \ No newline at end of file From ef9477cac0e1cab579e7412a01ef790dddb6f931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 24 Oct 2023 09:29:40 +0300 Subject: [PATCH 294/502] chore: v2 maintenance releases (#459) --- .github/workflows/release.yml | 1 + .releaserc.js | 1 + SECURITY.md | 13 ++++++------- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 644b23f..ab22f8d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,7 @@ name: Release on: push: branches: + - "2.11.x" - main - next tags-ignore: diff --git a/.releaserc.js b/.releaserc.js index e8eea8e..c87b1d1 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -1,5 +1,6 @@ module.exports = { branches: [ + {name: "2.11.x"}, {name: "main"}, {name: "next", prerelease: true}, ], diff --git a/SECURITY.md b/SECURITY.md index ec216f2..a32b842 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,13 +9,12 @@ We currently support the following version of the OIDC framework: | Version | Supported | Branch | Details | | -------- | ------------------ | ----------- | ------------------------------------ | | 0.x.x | :x: | | not maintained | -| <1.13 | :x: | | not maintained | -| 1.13.x | :lock: :warning: | [1.13.x][1] | security only, [community effort][2] | -| 2.x.x | :heavy_check_mark: | [main][3] | supported | -| 3.0.0-xx | :white_check_mark: | [next][4] | [developement branch][5] | +| <2.11 | :x: | | not maintained | +| 2.11.x | :lock: :warning: | [2.11.x][1] | security only, [community effort][2] | +| 3.x.x | :heavy_check_mark: | [main][3] | supported | +| 4.0.0-xx | :white_check_mark: | [next][4] | [development branch] | -[1]: https://github.com/zitadel/oidc/tree/1.13.x -[2]: https://github.com/zitadel/oidc/discussions/378 +[1]: https://github.com/zitadel/oidc/tree/2.11.x +[2]: https://github.com/zitadel/oidc/discussions/458 [3]: https://github.com/zitadel/oidc/tree/main [4]: https://github.com/zitadel/oidc/tree/next -[5]: https://github.com/zitadel/oidc/milestone/2 \ No newline at end of file From 164c5b28c7e3b83b8eccc5c9ea0aac41b1d15eaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 24 Oct 2023 10:16:58 +0300 Subject: [PATCH 295/502] fix(op): terminate session from request in legacy server (#465) --- pkg/op/server_legacy.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/op/server_legacy.go b/pkg/op/server_legacy.go index 5907e28..f373b9d 100644 --- a/pkg/op/server_legacy.go +++ b/pkg/op/server_legacy.go @@ -336,9 +336,14 @@ func (s *LegacyServer) EndSession(ctx context.Context, r *Request[oidc.EndSessio if err != nil { return nil, err } - err = s.provider.Storage().TerminateSession(ctx, session.UserID, session.ClientID) + redirect := session.RedirectURI + if fromRequest, ok := s.provider.Storage().(CanTerminateSessionFromRequest); ok { + redirect, err = fromRequest.TerminateSessionFromRequest(ctx, session) + } else { + err = s.provider.Storage().TerminateSession(ctx, session.UserID, session.ClientID) + } if err != nil { return nil, err } - return NewRedirect(session.RedirectURI), nil + return NewRedirect(redirect), nil } From bab53998599bfe56f1dc3c6a1c3f52b8a97b3a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 24 Oct 2023 10:20:02 +0300 Subject: [PATCH 296/502] feat(op): allow Legacy Server extension (#466) This change splits the constructor and registration of the Legacy Server. This allows it to be extended by struct embedding. --- example/server/exampleop/op.go | 2 +- pkg/op/server_http.go | 56 ++++++++++++++++------------- pkg/op/server_http_routes_test.go | 2 +- pkg/op/server_legacy.go | 58 ++++++++++++++++++++++++------- 4 files changed, 79 insertions(+), 39 deletions(-) diff --git a/example/server/exampleop/op.go b/example/server/exampleop/op.go index 74018da..baa2662 100644 --- a/example/server/exampleop/op.go +++ b/example/server/exampleop/op.go @@ -80,7 +80,7 @@ func SetupServer(issuer string, storage Storage, logger *slog.Logger, wrapServer handler := http.Handler(provider) if wrapServer { - handler = op.NewLegacyServer(provider, *op.DefaultEndpoints) + handler = op.RegisterLegacyServer(op.NewLegacyServer(provider, *op.DefaultEndpoints)) } // we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration) diff --git a/pkg/op/server_http.go b/pkg/op/server_http.go index 96ee7a5..750f7a9 100644 --- a/pkg/op/server_http.go +++ b/pkg/op/server_http.go @@ -25,11 +25,13 @@ func RegisterServer(server Server, endpoints Endpoints, options ...ServerOption) decoder.IgnoreUnknownKeys(true) ws := &webServer{ + router: chi.NewRouter(), server: server, endpoints: endpoints, decoder: decoder, logger: slog.Default(), } + ws.router.Use(cors.New(defaultCORSOptions).Handler) for _, option := range options { option(ws) @@ -45,7 +47,14 @@ type ServerOption func(s *webServer) // the Server's router. func WithHTTPMiddleware(m ...func(http.Handler) http.Handler) ServerOption { return func(s *webServer) { - s.middleware = m + s.router.Use(m...) + } +} + +// WithSetRouter allows customization or the Server's router. +func WithSetRouter(set func(chi.Router)) ServerOption { + return func(s *webServer) { + set(s.router) } } @@ -67,12 +76,15 @@ func WithFallbackLogger(logger *slog.Logger) ServerOption { } type webServer struct { - http.Handler - server Server - middleware []func(http.Handler) http.Handler - endpoints Endpoints - decoder httphelper.Decoder - logger *slog.Logger + server Server + router *chi.Mux + endpoints Endpoints + decoder httphelper.Decoder + logger *slog.Logger +} + +func (s *webServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.router.ServeHTTP(w, r) } func (s *webServer) getLogger(ctx context.Context) *slog.Logger { @@ -83,27 +95,23 @@ func (s *webServer) getLogger(ctx context.Context) *slog.Logger { } func (s *webServer) createRouter() { - router := chi.NewRouter() - router.Use(cors.New(defaultCORSOptions).Handler) - router.Use(s.middleware...) - router.HandleFunc(healthEndpoint, simpleHandler(s, s.server.Health)) - router.HandleFunc(readinessEndpoint, simpleHandler(s, s.server.Ready)) - router.HandleFunc(oidc.DiscoveryEndpoint, simpleHandler(s, s.server.Discovery)) + s.router.HandleFunc(healthEndpoint, simpleHandler(s, s.server.Health)) + s.router.HandleFunc(readinessEndpoint, simpleHandler(s, s.server.Ready)) + s.router.HandleFunc(oidc.DiscoveryEndpoint, simpleHandler(s, s.server.Discovery)) - s.endpointRoute(router, s.endpoints.Authorization, s.authorizeHandler) - s.endpointRoute(router, s.endpoints.DeviceAuthorization, s.withClient(s.deviceAuthorizationHandler)) - s.endpointRoute(router, s.endpoints.Token, s.tokensHandler) - s.endpointRoute(router, s.endpoints.Introspection, s.withClient(s.introspectionHandler)) - s.endpointRoute(router, s.endpoints.Userinfo, s.userInfoHandler) - s.endpointRoute(router, s.endpoints.Revocation, s.withClient(s.revocationHandler)) - s.endpointRoute(router, s.endpoints.EndSession, s.endSessionHandler) - s.endpointRoute(router, s.endpoints.JwksURI, simpleHandler(s, s.server.Keys)) - s.Handler = router + s.endpointRoute(s.endpoints.Authorization, s.authorizeHandler) + s.endpointRoute(s.endpoints.DeviceAuthorization, s.withClient(s.deviceAuthorizationHandler)) + s.endpointRoute(s.endpoints.Token, s.tokensHandler) + s.endpointRoute(s.endpoints.Introspection, s.withClient(s.introspectionHandler)) + s.endpointRoute(s.endpoints.Userinfo, s.userInfoHandler) + s.endpointRoute(s.endpoints.Revocation, s.withClient(s.revocationHandler)) + s.endpointRoute(s.endpoints.EndSession, s.endSessionHandler) + s.endpointRoute(s.endpoints.JwksURI, simpleHandler(s, s.server.Keys)) } -func (s *webServer) endpointRoute(router *chi.Mux, e *Endpoint, hf http.HandlerFunc) { +func (s *webServer) endpointRoute(e *Endpoint, hf http.HandlerFunc) { if e != nil { - router.HandleFunc(e.Relative(), hf) + s.router.HandleFunc(e.Relative(), hf) s.logger.Info("registered route", "endpoint", e.Relative()) } } diff --git a/pkg/op/server_http_routes_test.go b/pkg/op/server_http_routes_test.go index c7767d2..c50e989 100644 --- a/pkg/op/server_http_routes_test.go +++ b/pkg/op/server_http_routes_test.go @@ -32,7 +32,7 @@ func jwtProfile() (string, error) { } func TestServerRoutes(t *testing.T) { - server := op.NewLegacyServer(testProvider, *op.DefaultEndpoints) + server := op.RegisterLegacyServer(op.NewLegacyServer(testProvider, *op.DefaultEndpoints)) storage := testProvider.Storage().(routesTestStorage) ctx := op.ContextWithIssuer(context.Background(), testIssuer) diff --git a/pkg/op/server_legacy.go b/pkg/op/server_legacy.go index f373b9d..2006e90 100644 --- a/pkg/op/server_legacy.go +++ b/pkg/op/server_legacy.go @@ -10,37 +10,69 @@ import ( "github.com/zitadel/oidc/v3/pkg/oidc" ) -// LegacyServer is an implementation of [Server[] that -// simply wraps a [OpenIDProvider]. +// ExtendedLegacyServer allows embedding [LegacyServer] in a struct, +// so that its methods can be individually overridden. +// +// EXPERIMENTAL: may change until v4 +type ExtendedLegacyServer interface { + Server + Provider() OpenIDProvider + Endpoints() Endpoints +} + +// RegisterLegacyServer registers a [LegacyServer] or an extension thereof. +// It takes care of registering the IssuerFromRequest middleware +// and Authorization Callback Routes. +// Neither are part of the bare [Server] interface. +// +// EXPERIMENTAL: may change until v4 +func RegisterLegacyServer(s ExtendedLegacyServer, options ...ServerOption) http.Handler { + provider := s.Provider() + options = append(options, + WithHTTPMiddleware(intercept(provider.IssuerFromRequest)), + WithSetRouter(func(r chi.Router) { + r.HandleFunc(authCallbackPath(provider), authorizeCallbackHandler(provider)) + }), + ) + return RegisterServer(s, s.Endpoints(), options...) +} + +// LegacyServer is an implementation of [Server] that +// simply wraps an [OpenIDProvider]. // It can be used to transition from the former Provider/Storage // interfaces to the new Server interface. +// +// EXPERIMENTAL: may change until v4 type LegacyServer struct { UnimplementedServer provider OpenIDProvider endpoints Endpoints } -// NewLegacyServer wraps provider in a `Server` and returns a handler which is -// the Server's router. +// NewLegacyServer wraps provider in a `Server` implementation // // Only non-nil endpoints will be registered on the router. // Nil endpoints are disabled. // -// The passed endpoints is also set to the provider, -// to be consistent with the discovery config. +// The passed endpoints is also used for the discovery config, +// and endpoints already set to the provider are ignored. // Any `With*Endpoint()` option used on the provider is // therefore ineffective. -func NewLegacyServer(provider OpenIDProvider, endpoints Endpoints) http.Handler { - server := RegisterServer(&LegacyServer{ +// +// EXPERIMENTAL: may change until v4 +func NewLegacyServer(provider OpenIDProvider, endpoints Endpoints) *LegacyServer { + return &LegacyServer{ provider: provider, endpoints: endpoints, - }, endpoints, WithHTTPMiddleware(intercept(provider.IssuerFromRequest))) + } +} - router := chi.NewRouter() - router.Mount("/", server) - router.HandleFunc(authCallbackPath(provider), authorizeCallbackHandler(provider)) +func (s *LegacyServer) Provider() OpenIDProvider { + return s.provider +} - return router +func (s *LegacyServer) Endpoints() Endpoints { + return s.endpoints } func (s *LegacyServer) Health(_ context.Context, r *Request[struct{}]) (*Response, error) { From e5f0dca0e4807a81f8d04d775d22ee3409935b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 24 Oct 2023 18:06:04 +0300 Subject: [PATCH 297/502] fix: build callback url from server, not op (#468) --- pkg/op/server_legacy.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/op/server_legacy.go b/pkg/op/server_legacy.go index 2006e90..8939543 100644 --- a/pkg/op/server_legacy.go +++ b/pkg/op/server_legacy.go @@ -18,6 +18,7 @@ type ExtendedLegacyServer interface { Server Provider() OpenIDProvider Endpoints() Endpoints + AuthCallbackURL() func(context.Context, string) string } // RegisterLegacyServer registers a [LegacyServer] or an extension thereof. @@ -31,7 +32,7 @@ func RegisterLegacyServer(s ExtendedLegacyServer, options ...ServerOption) http. options = append(options, WithHTTPMiddleware(intercept(provider.IssuerFromRequest)), WithSetRouter(func(r chi.Router) { - r.HandleFunc(authCallbackPath(provider), authorizeCallbackHandler(provider)) + r.HandleFunc(s.Endpoints().Authorization.Relative()+authCallbackPathSuffix, authorizeCallbackHandler(provider)) }), ) return RegisterServer(s, s.Endpoints(), options...) @@ -75,6 +76,13 @@ func (s *LegacyServer) Endpoints() Endpoints { return s.endpoints } +// AuthCallbackURL builds the url for the redirect (with the requestID) after a successful login +func (s *LegacyServer) AuthCallbackURL() func(context.Context, string) string { + return func(ctx context.Context, requestID string) string { + return s.endpoints.Authorization.Absolute(IssuerFromContext(ctx)) + authCallbackPathSuffix + "?id=" + requestID + } +} + func (s *LegacyServer) Health(_ context.Context, r *Request[struct{}]) (*Response, error) { return NewResponse(Status{Status: "ok"}), nil } From 73a198207738a6d762961538e1ffd2adf4bc20a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 24 Oct 2023 18:07:20 +0300 Subject: [PATCH 298/502] fix(server): do not get client by id for introspection (#467) As introspection is a Oauth mechanism for resource servers only, it does not make sense to get an oidc client by ID. The original OP did not do this and now we make the server behavior similar. --- pkg/op/server.go | 4 ++-- pkg/op/server_http.go | 37 ++++++++++++++++++++++++------------ pkg/op/server_http_test.go | 13 ++++--------- pkg/op/server_legacy.go | 21 ++++++++++++++++++-- pkg/op/token_intospection.go | 5 +++++ 5 files changed, 55 insertions(+), 25 deletions(-) diff --git a/pkg/op/server.go b/pkg/op/server.go index a9cdcf5..829618c 100644 --- a/pkg/op/server.go +++ b/pkg/op/server.go @@ -127,7 +127,7 @@ type Server interface { // Introspect handles the OAuth 2.0 Token Introspection endpoint. // https://datatracker.ietf.org/doc/html/rfc7662 // The recommended Response Data type is [oidc.IntrospectionResponse]. - Introspect(context.Context, *ClientRequest[oidc.IntrospectionRequest]) (*Response, error) + Introspect(context.Context, *Request[IntrospectionRequest]) (*Response, error) // UserInfo handles the UserInfo endpoint and returns Claims about the authenticated End-User. // https://openid.net/specs/openid-connect-core-1_0.html#UserInfo @@ -329,7 +329,7 @@ func (UnimplementedServer) DeviceToken(ctx context.Context, r *ClientRequest[oid return nil, unimplementedGrantError(oidc.GrantTypeDeviceCode) } -func (UnimplementedServer) Introspect(ctx context.Context, r *ClientRequest[oidc.IntrospectionRequest]) (*Response, error) { +func (UnimplementedServer) Introspect(ctx context.Context, r *Request[IntrospectionRequest]) (*Response, error) { return nil, unimplementedError(r) } diff --git a/pkg/op/server_http.go b/pkg/op/server_http.go index 750f7a9..6d379c6 100644 --- a/pkg/op/server_http.go +++ b/pkg/op/server_http.go @@ -102,7 +102,7 @@ func (s *webServer) createRouter() { s.endpointRoute(s.endpoints.Authorization, s.authorizeHandler) s.endpointRoute(s.endpoints.DeviceAuthorization, s.withClient(s.deviceAuthorizationHandler)) s.endpointRoute(s.endpoints.Token, s.tokensHandler) - s.endpointRoute(s.endpoints.Introspection, s.withClient(s.introspectionHandler)) + s.endpointRoute(s.endpoints.Introspection, s.introspectionHandler) s.endpointRoute(s.endpoints.Userinfo, s.userInfoHandler) s.endpointRoute(s.endpoints.Revocation, s.withClient(s.revocationHandler)) s.endpointRoute(s.endpoints.EndSession, s.endSessionHandler) @@ -136,7 +136,21 @@ func (s *webServer) withClient(handler clientHandler) http.HandlerFunc { } func (s *webServer) verifyRequestClient(r *http.Request) (_ Client, err error) { - if err = r.ParseForm(); err != nil { + cc, err := s.parseClientCredentials(r) + if err != nil { + return nil, err + } + return s.server.VerifyClient(r.Context(), &Request[ClientCredentials]{ + Method: r.Method, + URL: r.URL, + Header: r.Header, + Form: r.Form, + Data: cc, + }) +} + +func (s *webServer) parseClientCredentials(r *http.Request) (_ *ClientCredentials, err error) { + if err := r.ParseForm(); err != nil { return nil, oidc.ErrInvalidRequest().WithDescription("error parsing form").WithParent(err) } cc := new(ClientCredentials) @@ -160,13 +174,7 @@ func (s *webServer) verifyRequestClient(r *http.Request) (_ Client, err error) { if cc.ClientAssertion != "" && cc.ClientAssertionType != oidc.ClientAssertionTypeJWTAssertion { return nil, oidc.ErrInvalidRequest().WithDescription("invalid client_assertion_type %s", cc.ClientAssertionType) } - return s.server.VerifyClient(r.Context(), &Request[ClientCredentials]{ - Method: r.Method, - URL: r.URL, - Header: r.Header, - Form: r.Form, - Data: cc, - }) + return cc, nil } func (s *webServer) authorizeHandler(w http.ResponseWriter, r *http.Request) { @@ -378,8 +386,13 @@ func (s *webServer) deviceTokenHandler(w http.ResponseWriter, r *http.Request, c resp.writeOut(w) } -func (s *webServer) introspectionHandler(w http.ResponseWriter, r *http.Request, client Client) { - if client.AuthMethod() == oidc.AuthMethodNone { +func (s *webServer) introspectionHandler(w http.ResponseWriter, r *http.Request) { + cc, err := s.parseClientCredentials(r) + if err != nil { + WriteError(w, r, err, s.getLogger(r.Context())) + return + } + if cc.ClientSecret == "" && cc.ClientAssertion == "" { WriteError(w, r, oidc.ErrInvalidClient().WithDescription("client must be authenticated"), s.getLogger(r.Context())) return } @@ -392,7 +405,7 @@ func (s *webServer) introspectionHandler(w http.ResponseWriter, r *http.Request, WriteError(w, r, oidc.ErrInvalidRequest().WithDescription("token missing"), s.getLogger(r.Context())) return } - resp, err := s.server.Introspect(r.Context(), newClientRequest(r, request, client)) + resp, err := s.server.Introspect(r.Context(), newRequest(r, &IntrospectionRequest{cc, request})) if err != nil { WriteError(w, r, err, s.getLogger(r.Context())) return diff --git a/pkg/op/server_http_test.go b/pkg/op/server_http_test.go index 86fe7ed..4eac4a0 100644 --- a/pkg/op/server_http_test.go +++ b/pkg/op/server_http_test.go @@ -1001,14 +1001,12 @@ func Test_webServer_introspectionHandler(t *testing.T) { tests := []struct { name string decoder httphelper.Decoder - client Client r *http.Request want webServerResult }{ { name: "decoder error", decoder: schema.NewDecoder(), - client: newClient(clientTypeUserAgent), r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), want: webServerResult{ wantStatus: http.StatusBadRequest, @@ -1018,8 +1016,7 @@ func Test_webServer_introspectionHandler(t *testing.T) { { name: "public client", decoder: testDecoder, - client: newClient(clientTypeNative), - r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("client_id=123")), want: webServerResult{ wantStatus: http.StatusBadRequest, wantBody: `{"error":"invalid_client", "error_description":"client must be authenticated"}`, @@ -1028,8 +1025,7 @@ func Test_webServer_introspectionHandler(t *testing.T) { { name: "token missing", decoder: testDecoder, - client: newClient(clientTypeWeb), - r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("client_id=123&client_secret=SECRET")), want: webServerResult{ wantStatus: http.StatusBadRequest, wantBody: `{"error":"invalid_request", "error_description":"token missing"}`, @@ -1038,8 +1034,7 @@ func Test_webServer_introspectionHandler(t *testing.T) { { name: "unimplemented Introspect called", decoder: testDecoder, - client: newClient(clientTypeWeb), - r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("token=xxx")), + r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("client_id=123&client_secret=SECRET&token=xxx")), want: webServerResult{ wantStatus: UnimplementedStatusCode, wantBody: `{"error":"server_error", "error_description":"/ not implemented on this server"}`, @@ -1053,7 +1048,7 @@ func Test_webServer_introspectionHandler(t *testing.T) { decoder: tt.decoder, logger: slog.Default(), } - runWebServerClientTest(t, s.introspectionHandler, tt.r, tt.client, tt.want) + runWebServerTest(t, s.introspectionHandler, tt.r, tt.want) }) } } diff --git a/pkg/op/server_legacy.go b/pkg/op/server_legacy.go index 8939543..deb1abc 100644 --- a/pkg/op/server_legacy.go +++ b/pkg/op/server_legacy.go @@ -315,13 +315,30 @@ func (s *LegacyServer) DeviceToken(ctx context.Context, r *ClientRequest[oidc.De return NewResponse(resp), nil } -func (s *LegacyServer) Introspect(ctx context.Context, r *ClientRequest[oidc.IntrospectionRequest]) (*Response, error) { +func (s *LegacyServer) authenticateResourceClient(ctx context.Context, cc *ClientCredentials) (string, error) { + if cc.ClientAssertion != "" { + if jp, ok := s.provider.(ClientJWTProfile); ok { + return ClientJWTAuth(ctx, oidc.ClientAssertionParams{ClientAssertion: cc.ClientAssertion}, jp) + } + return "", oidc.ErrInvalidClient().WithDescription("client_assertion not supported") + } + if err := s.provider.Storage().AuthorizeClientIDSecret(ctx, cc.ClientID, cc.ClientSecret); err != nil { + return "", oidc.ErrUnauthorizedClient().WithParent(err) + } + return cc.ClientID, nil +} + +func (s *LegacyServer) Introspect(ctx context.Context, r *Request[IntrospectionRequest]) (*Response, error) { + clientID, err := s.authenticateResourceClient(ctx, r.Data.ClientCredentials) + if err != nil { + return nil, err + } response := new(oidc.IntrospectionResponse) tokenID, subject, ok := getTokenIDAndSubject(ctx, s.provider, r.Data.Token) if !ok { return NewResponse(response), nil } - err := s.provider.Storage().SetIntrospectionFromToken(ctx, response, tokenID, subject, r.Client.GetID()) + err = s.provider.Storage().SetIntrospectionFromToken(ctx, response, tokenID, subject, clientID) if err != nil { return NewResponse(response), nil } diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go index 21b79c3..9c45ef8 100644 --- a/pkg/op/token_intospection.go +++ b/pkg/op/token_intospection.go @@ -65,3 +65,8 @@ func ParseTokenIntrospectionRequest(r *http.Request, introspector Introspector) return req.Token, clientID, nil } + +type IntrospectionRequest struct { + *ClientCredentials + *oidc.IntrospectionRequest +} From f6242db78dd056106c1033474e7ec524698fa22c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Oct 2023 09:33:53 +0300 Subject: [PATCH 299/502] chore(deps): bump github.com/zitadel/logging from 0.4.0 to 0.5.0 (#469) Bumps [github.com/zitadel/logging](https://github.com/zitadel/logging) from 0.4.0 to 0.5.0. - [Release notes](https://github.com/zitadel/logging/releases) - [Changelog](https://github.com/zitadel/logging/blob/main/.releaserc.js) - [Commits](https://github.com/zitadel/logging/compare/v0.4.0...v0.5.0) --- updated-dependencies: - dependency-name: github.com/zitadel/logging dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d3245eb..5000412 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/rs/cors v1.10.1 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 - github.com/zitadel/logging v0.4.0 + github.com/zitadel/logging v0.5.0 github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.19.0 go.opentelemetry.io/otel/trace v1.19.0 diff --git a/go.sum b/go.sum index c57f8da..69e5918 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/zitadel/logging v0.4.0 h1:lRAIFgaRoJpLNbsL7jtIYHcMDoEJP9QZB4GqMfl4xaA= -github.com/zitadel/logging v0.4.0/go.mod h1:6uALRJawpkkuUPCkgzfgcPR3c2N908wqnOnIrRelUFc= +github.com/zitadel/logging v0.5.0 h1:Kunouvqse/efXy4UDvFw5s3vP+Z4AlHo3y8wF7stXHA= +github.com/zitadel/logging v0.5.0/go.mod h1:IzP5fzwFhzzyxHkSmfF8dsyqFsQRJLLcQmwhIBzlGsE= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= From d58ab6a11584cf458cde09643588b2abeb7c5221 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Oct 2023 15:58:54 +0300 Subject: [PATCH 300/502] chore(deps): bump github.com/google/uuid from 1.3.1 to 1.4.0 (#470) Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.3.1 to 1.4.0. - [Release notes](https://github.com/google/uuid/releases) - [Changelog](https://github.com/google/uuid/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/uuid/compare/v1.3.1...v1.4.0) --- updated-dependencies: - dependency-name: github.com/google/uuid dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5000412..0460699 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/go-jose/go-jose/v3 v3.0.0 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 - github.com/google/uuid v1.3.1 + github.com/google/uuid v1.4.0 github.com/gorilla/securecookie v1.1.1 github.com/jeremija/gosubmit v0.2.7 github.com/muhlemmer/gu v0.3.1 diff --git a/go.sum b/go.sum index 69e5918..5fb6315 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,8 @@ github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gA github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc= From e260118fb2898a172fbbe9011637203268ba9860 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 09:48:06 +0200 Subject: [PATCH 301/502] chore(deps): bump github.com/gorilla/securecookie from 1.1.1 to 1.1.2 (#473) Bumps [github.com/gorilla/securecookie](https://github.com/gorilla/securecookie) from 1.1.1 to 1.1.2. - [Release notes](https://github.com/gorilla/securecookie/releases) - [Commits](https://github.com/gorilla/securecookie/compare/v1.1.1...v1.1.2) --- updated-dependencies: - dependency-name: github.com/gorilla/securecookie dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0460699..36d8931 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 github.com/google/uuid v1.4.0 - github.com/gorilla/securecookie v1.1.1 + github.com/gorilla/securecookie v1.1.2 github.com/jeremija/gosubmit v0.2.7 github.com/muhlemmer/gu v0.3.1 github.com/muhlemmer/httpforwarded v0.1.0 diff --git a/go.sum b/go.sum index 5fb6315..a4dad27 100644 --- a/go.sum +++ b/go.sum @@ -26,10 +26,11 @@ github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gA github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc= github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= From 60b80a73c4a49d4174c1b71d02a1165ca61628ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 09:48:41 +0200 Subject: [PATCH 302/502] chore(deps): bump golang.org/x/text from 0.13.0 to 0.14.0 (#474) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.13.0 to 0.14.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.13.0...v0.14.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 36d8931..684816e 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( go.opentelemetry.io/otel/trace v1.19.0 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 golang.org/x/oauth2 v0.13.0 - golang.org/x/text v0.13.0 + golang.org/x/text v0.14.0 ) require ( diff --git a/go.sum b/go.sum index a4dad27..0d0f0ad 100644 --- a/go.sum +++ b/go.sum @@ -94,8 +94,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= From 0ee3079b11b5feee946f003dcc21154d3e818dcd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:07:51 +0200 Subject: [PATCH 303/502] chore(deps): bump github.com/go-jose/go-jose/v3 from 3.0.0 to 3.0.1 (#475) Bumps [github.com/go-jose/go-jose/v3](https://github.com/go-jose/go-jose) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/go-jose/go-jose/releases) - [Changelog](https://github.com/go-jose/go-jose/blob/v3/CHANGELOG.md) - [Commits](https://github.com/go-jose/go-jose/compare/v3.0.0...v3.0.1) --- updated-dependencies: - dependency-name: github.com/go-jose/go-jose/v3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 684816e..9609a6d 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.19 require ( github.com/go-chi/chi/v5 v5.0.10 - github.com/go-jose/go-jose/v3 v3.0.0 + github.com/go-jose/go-jose/v3 v3.0.1 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 github.com/google/uuid v1.4.0 diff --git a/go.sum b/go.sum index 0d0f0ad..7453804 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ 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/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= -github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= +github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= From 0cfc32345aa7b93d52b875ac564dda9e5acbac36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Nov 2023 16:37:03 +0200 Subject: [PATCH 304/502] chore(deps): bump golang.org/x/oauth2 from 0.13.0 to 0.14.0 (#476) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.13.0 to 0.14.0. - [Commits](https://github.com/golang/oauth2/compare/v0.13.0...v0.14.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 9609a6d..20d73dc 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( go.opentelemetry.io/otel v1.19.0 go.opentelemetry.io/otel/trace v1.19.0 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 - golang.org/x/oauth2 v0.13.0 + golang.org/x/oauth2 v0.14.0 golang.org/x/text v0.14.0 ) @@ -32,9 +32,9 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/otel/metric v1.19.0 // indirect - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.13.0 // indirect + golang.org/x/crypto v0.15.0 // indirect + golang.org/x/net v0.18.0 // indirect + golang.org/x/sys v0.14.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 7453804..b8ac36c 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmY golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -75,11 +75,11 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= -golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= +golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -88,8 +88,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= From f7a0f7cb0b228c838aec40ae72356d022c3cba73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 10 Nov 2023 09:36:08 +0200 Subject: [PATCH 305/502] feat(op): create a JWT profile with a keyset --- pkg/op/verifier_jwt_profile.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index 3b13665..17f2b3e 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -17,11 +17,21 @@ import ( type JWTProfileVerifier struct { oidc.Verifier Storage JWTProfileKeyStorage + keySet oidc.KeySet CheckSubject func(request *oidc.JWTTokenRequest) error } // NewJWTProfileVerifier creates a oidc.Verifier for JWT Profile assertions (authorization grant and client authentication) func NewJWTProfileVerifier(storage JWTProfileKeyStorage, issuer string, maxAgeIAT, offset time.Duration, opts ...JWTProfileVerifierOption) *JWTProfileVerifier { + return newJWTProfileVerifier(storage, nil, issuer, maxAgeIAT, offset, opts...) +} + +// NewJWTProfileVerifier creates a oidc.Verifier for JWT Profile assertions (authorization grant and client authentication) +func NewJWTProfileVerifierKeySet(keySet oidc.KeySet, issuer string, maxAgeIAT, offset time.Duration, opts ...JWTProfileVerifierOption) *JWTProfileVerifier { + return newJWTProfileVerifier(nil, keySet, issuer, maxAgeIAT, offset, opts...) +} + +func newJWTProfileVerifier(storage JWTProfileKeyStorage, keySet oidc.KeySet, issuer string, maxAgeIAT, offset time.Duration, opts ...JWTProfileVerifierOption) *JWTProfileVerifier { j := &JWTProfileVerifier{ Verifier: oidc.Verifier{ Issuer: issuer, @@ -29,6 +39,7 @@ func NewJWTProfileVerifier(storage JWTProfileKeyStorage, issuer string, maxAgeIA Offset: offset, }, Storage: storage, + keySet: keySet, CheckSubject: SubjectIsIssuer, } @@ -78,7 +89,10 @@ func VerifyJWTAssertion(ctx context.Context, assertion string, v *JWTProfileVeri return nil, err } - keySet := &jwtProfileKeySet{storage: v.Storage, clientID: request.Issuer} + keySet := v.keySet + if keySet == nil { + keySet = &jwtProfileKeySet{storage: v.Storage, clientID: request.Issuer} + } if err = oidc.CheckSignature(ctx, assertion, payload, request, nil, keySet); err != nil { return nil, err } From 7475023a658947e12020b7e3e148d566ebed5812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 10 Nov 2023 14:18:08 +0200 Subject: [PATCH 306/502] feat(op): issuer from custom headers (#478) --- pkg/op/config.go | 58 ++++++++++++++++++++++++++++++------------- pkg/op/config_test.go | 58 ++++++++++++++++++++++++++++++------------- 2 files changed, 82 insertions(+), 34 deletions(-) diff --git a/pkg/op/config.go b/pkg/op/config.go index c383480..9fec7cc 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -54,7 +54,24 @@ type Configuration interface { type IssuerFromRequest func(r *http.Request) string func IssuerFromHost(path string) func(bool) (IssuerFromRequest, error) { - return issuerFromForwardedOrHost(path, false) + return issuerFromForwardedOrHost(path, new(issuerConfig)) +} + +type IssuerFromOption func(c *issuerConfig) + +// WithIssuerFromCustomHeaders can be used to customize the header names used. +// The same rules apply where the first successful host is returned. +func WithIssuerFromCustomHeaders(headers ...string) IssuerFromOption { + return func(c *issuerConfig) { + for i, h := range headers { + headers[i] = http.CanonicalHeaderKey(h) + } + c.headers = headers + } +} + +type issuerConfig struct { + headers []string } // IssuerFromForwardedOrHost tries to establish the Issuer based @@ -64,11 +81,18 @@ func IssuerFromHost(path string) func(bool) (IssuerFromRequest, error) { // If the Forwarded header is not present, no host field is found, // or there is a parser error the Request Host will be used as a fallback. // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded -func IssuerFromForwardedOrHost(path string) func(bool) (IssuerFromRequest, error) { - return issuerFromForwardedOrHost(path, true) +func IssuerFromForwardedOrHost(path string, opts ...IssuerFromOption) func(bool) (IssuerFromRequest, error) { + c := &issuerConfig{ + headers: []string{http.CanonicalHeaderKey("forwarded")}, + } + for _, opt := range opts { + opt(c) + } + + return issuerFromForwardedOrHost(path, c) } -func issuerFromForwardedOrHost(path string, parseForwarded bool) func(bool) (IssuerFromRequest, error) { +func issuerFromForwardedOrHost(path string, c *issuerConfig) func(bool) (IssuerFromRequest, error) { return func(allowInsecure bool) (IssuerFromRequest, error) { issuerPath, err := url.Parse(path) if err != nil { @@ -78,26 +102,26 @@ func issuerFromForwardedOrHost(path string, parseForwarded bool) func(bool) (Iss return nil, err } return func(r *http.Request) string { - if parseForwarded { - if host, ok := hostFromForwarded(r); ok { - return dynamicIssuer(host, path, allowInsecure) - } + if host, ok := hostFromForwarded(r, c.headers); ok { + return dynamicIssuer(host, path, allowInsecure) } return dynamicIssuer(r.Host, path, allowInsecure) }, nil } } -func hostFromForwarded(r *http.Request) (host string, ok bool) { - fwd, err := httpforwarded.ParseFromRequest(r) - if err != nil { - log.Printf("Err: issuer from forwarded header: %v", err) // TODO change to slog on next branch - return "", false +func hostFromForwarded(r *http.Request, headers []string) (host string, ok bool) { + for _, header := range headers { + hosts, err := httpforwarded.ParseParameter("host", r.Header[header]) + if err != nil { + log.Printf("Err: issuer from forwarded header: %v", err) // TODO change to slog on next branch + continue + } + if len(hosts) > 0 { + return hosts[0], true + } } - if fwd == nil || len(fwd["host"]) == 0 { - return "", false - } - return fwd["host"][0], true + return "", false } func StaticIssuer(issuer string) func(bool) (IssuerFromRequest, error) { diff --git a/pkg/op/config_test.go b/pkg/op/config_test.go index dcafc3a..d739348 100644 --- a/pkg/op/config_test.go +++ b/pkg/op/config_test.go @@ -1,6 +1,7 @@ package op import ( + "net/http" "net/http/httptest" "net/url" "testing" @@ -264,9 +265,10 @@ func TestIssuerFromHost(t *testing.T) { func TestIssuerFromForwardedOrHost(t *testing.T) { type args struct { - path string - target string - forwarded []string + path string + opts []IssuerFromOption + target string + header map[string][]string } type res struct { issuer string @@ -279,9 +281,9 @@ func TestIssuerFromForwardedOrHost(t *testing.T) { { "header parse error", args{ - path: "/custom/", - target: "https://issuer.com", - forwarded: []string{"~~~"}, + path: "/custom/", + target: "https://issuer.com", + header: map[string][]string{"Forwarded": {"~~~~"}}, }, res{ issuer: "https://issuer.com/custom/", @@ -303,9 +305,9 @@ func TestIssuerFromForwardedOrHost(t *testing.T) { args{ path: "/custom/", target: "https://issuer.com", - forwarded: []string{ + header: map[string][]string{"Forwarded": { `by=identifier;for=identifier;proto=https`, - }, + }}, }, res{ issuer: "https://issuer.com/custom/", @@ -316,9 +318,9 @@ func TestIssuerFromForwardedOrHost(t *testing.T) { args{ path: "/custom/", target: "https://issuer.com", - forwarded: []string{ + header: map[string][]string{"Forwarded": { `by=identifier;for=identifier;host=first.com;proto=https`, - }, + }}, }, res{ issuer: "https://first.com/custom/", @@ -329,9 +331,9 @@ func TestIssuerFromForwardedOrHost(t *testing.T) { args{ path: "/custom/", target: "https://issuer.com", - forwarded: []string{ + header: map[string][]string{"Forwarded": { `by=identifier;for=identifier;host=first.com;proto=https,host=second.com`, - }, + }}, }, res{ issuer: "https://first.com/custom/", @@ -342,23 +344,45 @@ func TestIssuerFromForwardedOrHost(t *testing.T) { args{ path: "/custom/", target: "https://issuer.com", - forwarded: []string{ + header: map[string][]string{"Forwarded": { `by=identifier;for=identifier;host=first.com;proto=https,host=second.com`, `by=identifier;for=identifier;host=third.com;proto=https`, - }, + }}, }, res{ issuer: "https://first.com/custom/", }, }, + { + "custom header first", + args{ + path: "/custom/", + target: "https://issuer.com", + header: map[string][]string{ + "Forwarded": { + `by=identifier;for=identifier;host=first.com;proto=https,host=second.com`, + `by=identifier;for=identifier;host=third.com;proto=https`, + }, + "X-Custom-Forwarded": { + `by=identifier;for=identifier;host=custom.com;proto=https,host=custom2.com`, + }, + }, + opts: []IssuerFromOption{ + WithIssuerFromCustomHeaders("x-custom-forwarded"), + }, + }, + res{ + issuer: "https://custom.com/custom/", + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - issuer, err := IssuerFromForwardedOrHost(tt.args.path)(false) + issuer, err := IssuerFromForwardedOrHost(tt.args.path, tt.args.opts...)(false) require.NoError(t, err) req := httptest.NewRequest("", tt.args.target, nil) - if tt.args.forwarded != nil { - req.Header["Forwarded"] = tt.args.forwarded + for k, v := range tt.args.header { + req.Header[http.CanonicalHeaderKey(k)] = v } assert.Equal(t, tt.res.issuer, issuer(req)) }) From d88c0ac296ecf635f921c6ad5c53ae98f6825713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 10 Nov 2023 16:26:54 +0200 Subject: [PATCH 307/502] fix(op): export NewProvider to allow customized issuer (#479) --- pkg/op/op.go | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/pkg/op/op.go b/pkg/op/op.go index 2bd130b..ba36c61 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -173,22 +173,52 @@ type Endpoints struct { // Successful logins should mark the request as authorized and redirect back to to // op.AuthCallbackURL(provider) which is probably /callback. On the redirect back // to the AuthCallbackURL, the request id should be passed as the "id" parameter. +// +// Deprecated: use [NewProvider] with an issuer function direct. func NewOpenIDProvider(issuer string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) { - return newProvider(config, storage, StaticIssuer(issuer), opOpts...) + return NewProvider(config, storage, StaticIssuer(issuer), opOpts...) } // NewForwardedOpenIDProvider tries to establishes the issuer from the request Host. +// +// Deprecated: use [NewProvider] with an issuer function direct. func NewDynamicOpenIDProvider(path string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) { - return newProvider(config, storage, IssuerFromHost(path), opOpts...) + return NewProvider(config, storage, IssuerFromHost(path), opOpts...) } // NewForwardedOpenIDProvider tries to establish the Issuer from a Forwarded request header, if it is set. // See [IssuerFromForwardedOrHost] for details. +// +// Deprecated: use [NewProvider] with an issuer function direct. func NewForwardedOpenIDProvider(path string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) { - return newProvider(config, storage, IssuerFromForwardedOrHost(path), opOpts...) + return NewProvider(config, storage, IssuerFromForwardedOrHost(path), opOpts...) } -func newProvider(config *Config, storage Storage, issuer func(bool) (IssuerFromRequest, error), opOpts ...Option) (_ *Provider, err error) { +// NewProvider creates a provider with a router on it's embedded http.Handler. +// Issuer is a function that must return the issuer on every request. +// Typically [StaticIssuer], [IssuerFromHost] or [IssuerFromForwardedOrHost] can be used. +// +// The router handles a suite of endpoints (some paths can be overridden): +// +// /healthz +// /ready +// /.well-known/openid-configuration +// /oauth/token +// /oauth/introspect +// /callback +// /authorize +// /userinfo +// /revoke +// /end_session +// /keys +// /device_authorization +// +// This does not include login. Login is handled with a redirect that includes the +// request ID. The redirect for logins is specified per-client by Client.LoginURL(). +// Successful logins should mark the request as authorized and redirect back to to +// op.AuthCallbackURL(provider) which is probably /callback. On the redirect back +// to the AuthCallbackURL, the request id should be passed as the "id" parameter. +func NewProvider(config *Config, storage Storage, issuer func(insecure bool) (IssuerFromRequest, error), opOpts ...Option) (_ *Provider, err error) { o := &Provider{ config: config, storage: storage, From f014796c45fffc8c3eca5fd0ac4b0ff7164b93c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 07:34:53 +0100 Subject: [PATCH 308/502] chore(deps): bump go.opentelemetry.io/otel/trace from 1.19.0 to 1.20.0 (#481) Bumps [go.opentelemetry.io/otel/trace](https://github.com/open-telemetry/opentelemetry-go) from 1.19.0 to 1.20.0. - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.19.0...v1.20.0) --- updated-dependencies: - dependency-name: go.opentelemetry.io/otel/trace dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 20d73dc..3425f28 100644 --- a/go.mod +++ b/go.mod @@ -17,8 +17,8 @@ require ( github.com/stretchr/testify v1.8.4 github.com/zitadel/logging v0.5.0 github.com/zitadel/schema v1.3.0 - go.opentelemetry.io/otel v1.19.0 - go.opentelemetry.io/otel/trace v1.19.0 + go.opentelemetry.io/otel v1.20.0 + go.opentelemetry.io/otel/trace v1.20.0 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 golang.org/x/oauth2 v0.14.0 golang.org/x/text v0.14.0 @@ -26,12 +26,12 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel/metric v1.19.0 // indirect + go.opentelemetry.io/otel/metric v1.20.0 // indirect golang.org/x/crypto v0.15.0 // indirect golang.org/x/net v0.18.0 // indirect golang.org/x/sys v0.14.0 // indirect diff --git a/go.sum b/go.sum index b8ac36c..af9cdb9 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNIT github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= @@ -20,7 +20,7 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo= github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= @@ -55,12 +55,12 @@ github.com/zitadel/logging v0.5.0 h1:Kunouvqse/efXy4UDvFw5s3vP+Z4AlHo3y8wF7stXHA github.com/zitadel/logging v0.5.0/go.mod h1:IzP5fzwFhzzyxHkSmfF8dsyqFsQRJLLcQmwhIBzlGsE= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= -go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= -go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= -go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= -go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= -go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= -go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +go.opentelemetry.io/otel v1.20.0 h1:vsb/ggIY+hUjD/zCAQHpzTmndPqv/ml2ArbsbfBYTAc= +go.opentelemetry.io/otel v1.20.0/go.mod h1:oUIGj3D77RwJdM6PPZImDpSZGDvkD9fhesHny69JFrs= +go.opentelemetry.io/otel/metric v1.20.0 h1:ZlrO8Hu9+GAhnepmRGhSU7/VkpjrNowxRN9GyKR4wzA= +go.opentelemetry.io/otel/metric v1.20.0/go.mod h1:90DRw3nfK4D7Sm/75yQ00gTJxtkBxX+wu6YaNymbpVM= +go.opentelemetry.io/otel/trace v1.20.0 h1:+yxVAPZPbQhbC3OfAkeIVTky6iTFpcr4SiY9om7mXSQ= +go.opentelemetry.io/otel/trace v1.20.0/go.mod h1:HJSK7F/hA5RlzpZ0zKDCHCDHm556LCDtKaAo6JmBFUU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= From f6bd17e8db69ff62ad30dca419d7065dafe2c4dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Mon, 13 Nov 2023 19:28:01 +0200 Subject: [PATCH 309/502] correct comment --- pkg/op/verifier_jwt_profile.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index 17f2b3e..38b8ee4 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -26,7 +26,7 @@ func NewJWTProfileVerifier(storage JWTProfileKeyStorage, issuer string, maxAgeIA return newJWTProfileVerifier(storage, nil, issuer, maxAgeIAT, offset, opts...) } -// NewJWTProfileVerifier creates a oidc.Verifier for JWT Profile assertions (authorization grant and client authentication) +// NewJWTProfileVerifierKeySet creates a oidc.Verifier for JWT Profile assertions (authorization grant and client authentication) func NewJWTProfileVerifierKeySet(keySet oidc.KeySet, issuer string, maxAgeIAT, offset time.Duration, opts ...JWTProfileVerifierOption) *JWTProfileVerifier { return newJWTProfileVerifier(nil, keySet, issuer, maxAgeIAT, offset, opts...) } From ce55068aa974ce374632a544ad1f28839b8f66a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 10:03:56 +0200 Subject: [PATCH 310/502] chore(deps): bump go.opentelemetry.io/otel from 1.20.0 to 1.21.0 (#488) Bumps [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) from 1.20.0 to 1.21.0. - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.20.0...v1.21.0) --- updated-dependencies: - dependency-name: go.opentelemetry.io/otel dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 3425f28..ab4a01f 100644 --- a/go.mod +++ b/go.mod @@ -17,8 +17,8 @@ require ( github.com/stretchr/testify v1.8.4 github.com/zitadel/logging v0.5.0 github.com/zitadel/schema v1.3.0 - go.opentelemetry.io/otel v1.20.0 - go.opentelemetry.io/otel/trace v1.20.0 + go.opentelemetry.io/otel v1.21.0 + go.opentelemetry.io/otel/trace v1.21.0 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 golang.org/x/oauth2 v0.14.0 golang.org/x/text v0.14.0 @@ -31,7 +31,7 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel/metric v1.20.0 // indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect golang.org/x/crypto v0.15.0 // indirect golang.org/x/net v0.18.0 // indirect golang.org/x/sys v0.14.0 // indirect diff --git a/go.sum b/go.sum index af9cdb9..bbdb5b9 100644 --- a/go.sum +++ b/go.sum @@ -55,12 +55,12 @@ github.com/zitadel/logging v0.5.0 h1:Kunouvqse/efXy4UDvFw5s3vP+Z4AlHo3y8wF7stXHA github.com/zitadel/logging v0.5.0/go.mod h1:IzP5fzwFhzzyxHkSmfF8dsyqFsQRJLLcQmwhIBzlGsE= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= -go.opentelemetry.io/otel v1.20.0 h1:vsb/ggIY+hUjD/zCAQHpzTmndPqv/ml2ArbsbfBYTAc= -go.opentelemetry.io/otel v1.20.0/go.mod h1:oUIGj3D77RwJdM6PPZImDpSZGDvkD9fhesHny69JFrs= -go.opentelemetry.io/otel/metric v1.20.0 h1:ZlrO8Hu9+GAhnepmRGhSU7/VkpjrNowxRN9GyKR4wzA= -go.opentelemetry.io/otel/metric v1.20.0/go.mod h1:90DRw3nfK4D7Sm/75yQ00gTJxtkBxX+wu6YaNymbpVM= -go.opentelemetry.io/otel/trace v1.20.0 h1:+yxVAPZPbQhbC3OfAkeIVTky6iTFpcr4SiY9om7mXSQ= -go.opentelemetry.io/otel/trace v1.20.0/go.mod h1:HJSK7F/hA5RlzpZ0zKDCHCDHm556LCDtKaAo6JmBFUU= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= From 7b64687990dc58def256b2613de97ba5d05c6384 Mon Sep 17 00:00:00 2001 From: Kory Prince Date: Fri, 17 Nov 2023 07:33:48 -0600 Subject: [PATCH 311/502] feat: Allow CORS policy to be configured (#484) * Add configurable CORS policy in OpenIDProvider * Add configurable CORS policy to Server * remove duplicated CORS middleware * Allow nil CORS policy to be set to disable CORS middleware * create a separate handler on webServer so type assertion works in tests --- pkg/op/op.go | 27 +++++++++++++++++++++++++-- pkg/op/server_http.go | 17 +++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/pkg/op/op.go b/pkg/op/op.go index ba36c61..939ebf8 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -97,9 +97,19 @@ type OpenIDProvider interface { type HttpInterceptor func(http.Handler) http.Handler +type corsOptioner interface { + CORSOptions() *cors.Options +} + func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) chi.Router { router := chi.NewRouter() - router.Use(cors.New(defaultCORSOptions).Handler) + if co, ok := o.(corsOptioner); ok { + if opts := co.CORSOptions(); opts != nil { + router.Use(cors.New(*opts).Handler) + } + } else { + router.Use(cors.New(defaultCORSOptions).Handler) + } router.Use(intercept(o.IssuerFromRequest, interceptors...)) router.HandleFunc(healthEndpoint, healthHandler) router.HandleFunc(readinessEndpoint, readyHandler(o.Probes())) @@ -224,6 +234,7 @@ func NewProvider(config *Config, storage Storage, issuer func(insecure bool) (Is storage: storage, endpoints: DefaultEndpoints, timer: make(<-chan time.Time), + corsOpts: &defaultCORSOptions, logger: slog.Default(), } @@ -268,6 +279,7 @@ type Provider struct { timer <-chan time.Time accessTokenVerifierOpts []AccessTokenVerifierOpt idTokenHintVerifierOpts []IDTokenHintVerifierOpt + corsOpts *cors.Options logger *slog.Logger } @@ -427,6 +439,10 @@ func (o *Provider) Probes() []ProbesFn { } } +func (o *Provider) CORSOptions() *cors.Options { + return o.corsOpts +} + func (o *Provider) Logger() *slog.Logger { return o.logger } @@ -587,6 +603,13 @@ func WithIDTokenHintVerifierOpts(opts ...IDTokenHintVerifierOpt) Option { } } +func WithCORSOptions(opts *cors.Options) Option { + return func(o *Provider) error { + o.corsOpts = opts + return nil + } +} + // WithLogger lets a logger other than slog.Default(). // // EXPERIMENTAL: Will change to log/slog import after we drop support for Go 1.20 @@ -603,6 +626,6 @@ func intercept(i IssuerFromRequest, interceptors ...HttpInterceptor) func(handle for i := len(interceptors) - 1; i >= 0; i-- { handler = interceptors[i](handler) } - return cors.New(defaultCORSOptions).Handler(issuerInterceptor.Handler(handler)) + return issuerInterceptor.Handler(handler) } } diff --git a/pkg/op/server_http.go b/pkg/op/server_http.go index 6d379c6..2220e44 100644 --- a/pkg/op/server_http.go +++ b/pkg/op/server_http.go @@ -29,15 +29,19 @@ func RegisterServer(server Server, endpoints Endpoints, options ...ServerOption) server: server, endpoints: endpoints, decoder: decoder, + corsOpts: &defaultCORSOptions, logger: slog.Default(), } - ws.router.Use(cors.New(defaultCORSOptions).Handler) for _, option := range options { option(ws) } ws.createRouter() + ws.handler = ws.router + if ws.corsOpts != nil { + ws.handler = cors.New(*ws.corsOpts).Handler(ws.router) + } return ws } @@ -66,6 +70,13 @@ func WithDecoder(decoder httphelper.Decoder) ServerOption { } } +// WithServerCORSOptions sets the CORS policy for the Server's router. +func WithServerCORSOptions(opts *cors.Options) ServerOption { + return func(s *webServer) { + s.corsOpts = opts + } +} + // WithFallbackLogger overrides the fallback logger, which // is used when no logger was found in the context. // Defaults to [slog.Default]. @@ -78,13 +89,15 @@ func WithFallbackLogger(logger *slog.Logger) ServerOption { type webServer struct { server Server router *chi.Mux + handler http.Handler endpoints Endpoints decoder httphelper.Decoder + corsOpts *cors.Options logger *slog.Logger } func (s *webServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { - s.router.ServeHTTP(w, r) + s.handler.ServeHTTP(w, r) } func (s *webServer) getLogger(ctx context.Context) *slog.Logger { From 7d0cdec925d31756e92f73ae45864dec9c07656b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Mon, 20 Nov 2023 13:40:42 +0100 Subject: [PATCH 312/502] fix(examples): Offer Storage with non-global client (#489) --- example/server/storage/storage.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index 1a04f4f..b556828 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -90,6 +90,10 @@ func (s *publicKey) Key() any { } func NewStorage(userStore UserStore) *Storage { + return NewStorageWithClients(userStore, clients) +} + +func NewStorageWithClients(userStore UserStore, clients map[string]*Client) *Storage { key, _ := rsa.GenerateKey(rand.Reader, 2048) return &Storage{ authRequests: make(map[string]*AuthRequest), From 4d05eade5ec3cab061695ea56d1e6a3e29d66b1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 09:59:39 +0200 Subject: [PATCH 313/502] chore(deps): bump golang.org/x/oauth2 from 0.14.0 to 0.15.0 (#492) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.14.0 to 0.15.0. - [Commits](https://github.com/golang/oauth2/compare/v0.14.0...v0.15.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index ab4a01f..d78f707 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( go.opentelemetry.io/otel v1.21.0 go.opentelemetry.io/otel/trace v1.21.0 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 - golang.org/x/oauth2 v0.14.0 + golang.org/x/oauth2 v0.15.0 golang.org/x/text v0.14.0 ) @@ -32,9 +32,9 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/otel/metric v1.21.0 // indirect - golang.org/x/crypto v0.15.0 // indirect - golang.org/x/net v0.18.0 // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.15.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index bbdb5b9..3bf2861 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -75,11 +75,11 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= -golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= +golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= +golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -88,8 +88,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= From fe3e02b80aaf9827e0f18c04ac517911bd1d80f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 5 Dec 2023 07:40:16 +0200 Subject: [PATCH 314/502] feat(rp): client credentials grant (#494) This change adds Client Credentials grant to the Relying Party. As specified in [RFC 6749, section 4.4](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4) --- pkg/client/integration_test.go | 25 +++++++++++++++++++++++++ pkg/client/rp/relying_party.go | 25 ++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/pkg/client/integration_test.go b/pkg/client/integration_test.go index ec4d57b..7d4cd9e 100644 --- a/pkg/client/integration_test.go +++ b/pkg/client/integration_test.go @@ -323,6 +323,31 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID, return provider, tokens } +func TestClientCredentials(t *testing.T) { + targetURL := "http://local-site" + exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL)) + var dh deferredHandler + opServer := httptest.NewServer(&dh) + defer opServer.Close() + t.Logf("auth server at %s", opServer.URL) + dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger, true) + + provider, err := rp.NewRelyingPartyOIDC( + CTX, + opServer.URL, + "sid1", + "verysecret", + targetURL, + []string{"openid"}, + ) + require.NoError(t, err, "new rp") + + token, err := rp.ClientCredentials(CTX, provider, nil) + require.NoError(t, err, "ClientCredentials call") + require.NotNil(t, token) + assert.NotEmpty(t, token.AccessToken) +} + func TestErrorFromPromptNone(t *testing.T) { jar, err := cookiejar.New(nil) require.NoError(t, err, "create cookie jar") diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index c6ae2db..5899af0 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -14,6 +14,7 @@ import ( "github.com/zitadel/logging" "golang.org/x/exp/slog" "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" "github.com/zitadel/oidc/v3/pkg/client" httphelper "github.com/zitadel/oidc/v3/pkg/http" @@ -416,12 +417,34 @@ func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingP return verifyTokenResponse[C](ctx, token, rp) } +// ClientCredentials requests an access token using the `client_credentials` grant, +// as defined in [RFC 6749, section 4.4]. +// +// As there is no user associated to the request an ID Token can never be returned. +// Client Credentials are undefined in OpenID Connect and is a pure OAuth2 grant. +// Furthermore the server SHOULD NOT return a refresh token. +// +// [RFC 6749, section 4.4]: https://datatracker.ietf.org/doc/html/rfc6749#section-4.4 +func ClientCredentials(ctx context.Context, rp RelyingParty, endpointParams url.Values) (token *oauth2.Token, err error) { + ctx = logCtxWithRPData(ctx, rp, "function", "ClientCredentials") + ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient()) + config := clientcredentials.Config{ + ClientID: rp.OAuthConfig().ClientID, + ClientSecret: rp.OAuthConfig().ClientSecret, + TokenURL: rp.OAuthConfig().Endpoint.TokenURL, + Scopes: rp.OAuthConfig().Scopes, + EndpointParams: endpointParams, + AuthStyle: rp.OAuthConfig().Endpoint.AuthStyle, + } + return config.Token(ctx) +} + type CodeExchangeCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) // CodeExchangeHandler extends the `CodeExchange` method with a http handler // including cookie handling for secure `state` transfer // and optional PKCE code verifier checking. -// Custom paramaters can optionally be set to the token URL. +// Custom parameters can optionally be set to the token URL. func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { state, err := tryReadStateCookie(w, r, rp) From 3a4d44cae714b93ad3c45b4d43c407380db9525a Mon Sep 17 00:00:00 2001 From: Oleksandr Shepetko Date: Tue, 5 Dec 2023 17:15:59 +0200 Subject: [PATCH 315/502] fix(crypto): nil pointer dereference in crypto.BytesToPrivateKey (#491) (#493) --- pkg/crypto/key.go | 13 ++++++--- pkg/crypto/key_test.go | 62 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 pkg/crypto/key_test.go diff --git a/pkg/crypto/key.go b/pkg/crypto/key.go index d75d1ab..79e2046 100644 --- a/pkg/crypto/key.go +++ b/pkg/crypto/key.go @@ -4,14 +4,19 @@ import ( "crypto/rsa" "crypto/x509" "encoding/pem" + "errors" ) -func BytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) { - block, _ := pem.Decode(priv) - b := block.Bytes - key, err := x509.ParsePKCS1PrivateKey(b) +func BytesToPrivateKey(b []byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(b) + if block == nil { + return nil, errors.New("PEM decode failed") + } + + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { return nil, err } + return key, nil } diff --git a/pkg/crypto/key_test.go b/pkg/crypto/key_test.go new file mode 100644 index 0000000..23ebdc0 --- /dev/null +++ b/pkg/crypto/key_test.go @@ -0,0 +1,62 @@ +package crypto_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/zitadel/oidc/v3/pkg/crypto" +) + +func TestBytesToPrivateKey(tt *testing.T) { + tt.Run("PEMDecodeError", func(t *testing.T) { + _, err := crypto.BytesToPrivateKey([]byte("The non-PEM sequence")) + assert.EqualError(t, err, "PEM decode failed") + }) + + tt.Run("InvalidKeyFormat", func(t *testing.T) { + _, err := crypto.BytesToPrivateKey([]byte(`-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfaDB7pK/fmP/I +7IusSK8lTCBnPZghqIbVLt2QHYAMoEF1CaF4F4rxo2vl1Mt8gwsq4T3osQFZMvnL +YHb7KNyUoJgTjLxJQADv2u4Q3U38heAzK5Tp4ry4MCnuyJIqAPK1GiruwEq4zQrx ++WzVix8otO37SuW9tzklqlNGMiAYBL0TBKHvS5XMbjP1idBMB8erMz29w/TVQnEB +Kj0vCdZjrbVPKygptt5kcSrL5f4xCZwU+ufz7cp0GLwpRMJ+shG9YJJFBxb0itPF +sy51vAyEtdBC7jgAU96ZVeQ06nryDq1D2EpoVMElqNyL46Jo3lnKbGquGKzXzQYU +BN32/scDAgMBAAECggEBAJE/mo3PLgILo2YtQ8ekIxNVHmF0Gl7w9IrjvTdH6hmX +HI3MTLjkmtI7GmG9V/0IWvCjdInGX3grnrjWGRQZ04QKIQgPQLFuBGyJjEsJm7nx +MqztlS7YTyV1nX/aenSTkJO8WEpcJLnm+4YoxCaAMdAhrIdBY71OamALpv1bRysa +FaiCGcemT2yqZn0GqIS8O26Tz5zIqrTN2G1eSmgh7DG+7FoddMz35cute8R10xUG +hF5YU+6fcXiRQ/Kh7nlxelPGqdZFPMk7LpVHzkQKwdJ+N0P23lPDIfNsvpG1n0OP +3g5km7gHSrSU2yZ3eFl6DB9x1IFNS9BaQQuSxYJtKwECgYEA1C8jjzpXZDLvlYsV +2jlMzkrbsIrX2dzblVrNsPs2jRbjYU8mg2DUDO6lOhtxHfqZG6sO+gmWi/zvoy9l +yolGbXe1Jqx66p9fznIcecSwar8+ACa356Wk74Nt1PlBOfCMqaJnYLOLaFJa29Vy +u5ClZVzKd5AVXl7yFVd4XfLv/WECgYEAwFMMtFoasdF92c0d31rZ1uoPOtFz6xq6 +uQggdm5zzkhnfwUAGqppS/u1CHcJ7T/74++jLbFTsaohGr4jEzWSGvJpomEUChy3 +r25YofMclUhJ5pCEStsLtqiCR1Am6LlI8HMdBEP1QDgEC5q8bQW4+UHuew1E1zxz +osZOhe09WuMCgYEA0G9aFCnwjUqIFjQiDFP7gi8BLqTFs4uE3Wvs4W11whV42i+B +ms90nxuTjchFT3jMDOT1+mOO0wdudLRr3xEI8SIF/u6ydGaJG+j21huEXehtxIJE +aDdNFcfbDbqo+3y1ATK7MMBPMvSrsoY0hdJq127WqasNgr3sO1DIuima3SECgYEA +nkM5TyhekzlbIOHD1UsDu/D7+2DkzPE/+oePfyXBMl0unb3VqhvVbmuBO6gJiSx/ +8b//PdiQkMD5YPJaFrKcuoQFHVRZk0CyfzCEyzAts0K7XXpLAvZiGztriZeRjSz7 +srJnjF0H8oKmAY6hw+1Tm/n/b08p+RyL48TgVSE2vhUCgYA3BWpkD4PlCcn/FZsq +OrLFyFXI6jIaxskFtsRW1IxxIlAdZmxfB26P/2gx6VjLdxJI/RRPkJyEN2dP7CbR +BDjb565dy1O9D6+UrY70Iuwjz+OcALRBBGTaiF2pLn6IhSzNI2sy/tXX8q8dBlg9 +OFCrqT/emes3KytTPfa5NZtYeQ== +-----END PRIVATE KEY-----`)) + assert.EqualError(t, err, "x509: failed to parse private key (use ParsePKCS8PrivateKey instead for this key format)") + }) + + tt.Run("Ok", func(t *testing.T) { + key, err := crypto.BytesToPrivateKey([]byte(`-----BEGIN RSA PRIVATE KEY----- +MIIBOgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu +KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm +o3qGy0t6z09AIJtH+5OeRV1be+N4cDYJKffGzDa88vQENZiRm0GRq6a+HPGQMd2k +TQIhAKMSvzIBnni7ot/OSie2TmJLY4SwTQAevXysE2RbFDYdAiEBCUEaRQnMnbp7 +9mxDXDf6AU0cN/RPBjb9qSHDcWZHGzUCIG2Es59z8ugGrDY+pxLQnwfotadxd+Uy +v/Ow5T0q5gIJAiEAyS4RaI9YG8EWx/2w0T67ZUVAw8eOMB6BIUg0Xcu+3okCIBOs +/5OiPgoTdSy7bcF9IGpSE8ZgGKzgYQVZeN97YE00 +-----END RSA PRIVATE KEY-----`)) + assert.NoError(t, err) + assert.NotNil(t, key) + }) +} From ed21cdd4cea8eb3942a9a78f6a4b554a52ecfb62 Mon Sep 17 00:00:00 2001 From: mffap Date: Wed, 6 Dec 2023 11:51:24 +0200 Subject: [PATCH 316/502] docs: update features client credential grant (#497) Introduced with https://github.com/zitadel/oidc/pull/494 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f9ec7ce..7f1a610 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid | Code Flow | yes | yes | OpenID Connect Core 1.0, [Section 3.1][1] | | Implicit Flow | no[^1] | yes | OpenID Connect Core 1.0, [Section 3.2][2] | | Hybrid Flow | no | not yet | OpenID Connect Core 1.0, [Section 3.3][3] | -| Client Credentials | not yet | yes | OpenID Connect Core 1.0, [Section 9][4] | +| Client Credentials | yes | yes | OpenID Connect Core 1.0, [Section 9][4] | | Refresh Token | yes | yes | OpenID Connect Core 1.0, [Section 12][5] | | Discovery | yes | yes | OpenID Connect [Discovery][6] 1.0 | | JWT Profile | yes | yes | [RFC 7523][7] | From 9d12d1d900f30a2eed3a8e60b5e33988758409bf Mon Sep 17 00:00:00 2001 From: Stephen Andary Date: Thu, 7 Dec 2023 10:36:03 -0500 Subject: [PATCH 317/502] feat(op): PKCE Verification in Legacy Server when AuthMethod is not NONE and CodeVerifier is not Empty (#496) * add logic for legacy server pkce verification when auth method is not None, and code verifier is not empty. * update per Tim's direction --- pkg/op/server_legacy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/op/server_legacy.go b/pkg/op/server_legacy.go index deb1abc..a851a2a 100644 --- a/pkg/op/server_legacy.go +++ b/pkg/op/server_legacy.go @@ -205,7 +205,7 @@ func (s *LegacyServer) CodeExchange(ctx context.Context, r *ClientRequest[oidc.A if err != nil { return nil, err } - if r.Client.AuthMethod() == oidc.AuthMethodNone { + if r.Client.AuthMethod() == oidc.AuthMethodNone || r.Data.CodeVerifier != "" { if err = AuthorizeCodeChallenge(r.Data.CodeVerifier, authReq.GetCodeChallenge()); err != nil { return nil, err } From 9c582989d9f46eb4d3b1cbfddf44e2734dd8c196 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 11:58:03 +0200 Subject: [PATCH 318/502] chore(deps): bump actions/setup-go from 4 to 5 (#498) Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-go dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab22f8d..42f8d4a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... From bca8833c15fa96da2ed04aa3e5830ae0c2d89ac0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 11:59:11 +0200 Subject: [PATCH 319/502] chore(deps): bump github.com/google/uuid from 1.4.0 to 1.5.0 (#499) Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.4.0 to 1.5.0. - [Release notes](https://github.com/google/uuid/releases) - [Changelog](https://github.com/google/uuid/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/uuid/compare/v1.4.0...v1.5.0) --- updated-dependencies: - dependency-name: github.com/google/uuid dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d78f707..62d42a5 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/go-jose/go-jose/v3 v3.0.1 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 - github.com/google/uuid v1.4.0 + github.com/google/uuid v1.5.0 github.com/gorilla/securecookie v1.1.2 github.com/jeremija/gosubmit v0.2.7 github.com/muhlemmer/gu v0.3.1 diff --git a/go.sum b/go.sum index 3bf2861..8cff8d2 100644 --- a/go.sum +++ b/go.sum @@ -27,8 +27,8 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc= From 7bdaf9c71db5666c15a79f0a7b68865c75d4be2c Mon Sep 17 00:00:00 2001 From: snow <47487241+snowkat@users.noreply.github.com> Date: Sun, 17 Dec 2023 04:06:42 -0800 Subject: [PATCH 320/502] feat(op): User-configurable claims_supported (#495) * User-configurable claims_supported * Use op.SupportedClaims instead of interface --- pkg/op/discovery.go | 30 +++++------------------------- pkg/op/op.go | 28 ++++++++++++++++++++++++++++ pkg/op/op_test.go | 1 + 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 8251261..6af1674 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -213,32 +213,12 @@ func AuthMethodsRevocationEndpoint(c Configuration) []oidc.AuthMethod { } func SupportedClaims(c Configuration) []string { - return []string{ // TODO: config - "sub", - "aud", - "exp", - "iat", - "iss", - "auth_time", - "nonce", - "acr", - "amr", - "c_hash", - "at_hash", - "act", - "scopes", - "client_id", - "azp", - "preferred_username", - "name", - "family_name", - "given_name", - "locale", - "email", - "email_verified", - "phone_number", - "phone_number_verified", + provider, ok := c.(*Provider) + if ok && provider.config.SupportedClaims != nil { + return provider.config.SupportedClaims } + + return DefaultSupportedClaims } func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod { diff --git a/pkg/op/op.go b/pkg/op/op.go index 939ebf8..fdc073c 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -45,6 +45,33 @@ var ( DeviceAuthorization: NewEndpoint(defaultDeviceAuthzEndpoint), } + DefaultSupportedClaims = []string{ + "sub", + "aud", + "exp", + "iat", + "iss", + "auth_time", + "nonce", + "acr", + "amr", + "c_hash", + "at_hash", + "act", + "scopes", + "client_id", + "azp", + "preferred_username", + "name", + "family_name", + "given_name", + "locale", + "email", + "email_verified", + "phone_number", + "phone_number_verified", + } + defaultCORSOptions = cors.Options{ AllowCredentials: true, AllowedHeaders: []string{ @@ -146,6 +173,7 @@ type Config struct { GrantTypeRefreshToken bool RequestObjectSupported bool SupportedUILocales []language.Tag + SupportedClaims []string DeviceAuthorization DeviceAuthorizationConfig } diff --git a/pkg/op/op_test.go b/pkg/op/op_test.go index 062fcfe..f97f666 100644 --- a/pkg/op/op_test.go +++ b/pkg/op/op_test.go @@ -30,6 +30,7 @@ var ( AuthMethodPrivateKeyJWT: true, GrantTypeRefreshToken: true, RequestObjectSupported: true, + SupportedClaims: op.DefaultSupportedClaims, SupportedUILocales: []language.Tag{language.English}, DeviceAuthorization: op.DeviceAuthorizationConfig{ Lifetime: 5 * time.Minute, From b300027cd755317eecef840dd2db2b75c991b008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Mon, 18 Dec 2023 09:39:39 +0200 Subject: [PATCH 321/502] feat(op): ID token for device authorization grant (#500) --- pkg/op/device.go | 91 ++++++++++++++++++++++++++++------------ pkg/op/device_test.go | 93 +++++++++++++++++++++++++++++++++++++++++ pkg/op/server_legacy.go | 9 +--- pkg/op/storage.go | 9 ---- pkg/op/token.go | 2 + 5 files changed, 162 insertions(+), 42 deletions(-) diff --git a/pkg/op/device.go b/pkg/op/device.go index 813c3f5..1b86d04 100644 --- a/pkg/op/device.go +++ b/pkg/op/device.go @@ -14,6 +14,7 @@ import ( httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" + strs "github.com/zitadel/oidc/v3/pkg/strings" ) type DeviceAuthorizationConfig struct { @@ -185,24 +186,6 @@ func NewUserCode(charSet []rune, charAmount, dashInterval int) (string, error) { return buf.String(), nil } -type deviceAccessTokenRequest struct { - subject string - audience []string - scopes []string -} - -func (r *deviceAccessTokenRequest) GetSubject() string { - return r.subject -} - -func (r *deviceAccessTokenRequest) GetAudience() []string { - return r.audience -} - -func (r *deviceAccessTokenRequest) GetScopes() []string { - return r.scopes -} - func DeviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { ctx, span := tracer.Start(r.Context(), "DeviceAccessToken") defer span.End() @@ -229,7 +212,7 @@ func deviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchang if err != nil { return err } - state, err := CheckDeviceAuthorizationState(ctx, clientID, req.DeviceCode, exchanger) + tokenRequest, err := CheckDeviceAuthorizationState(ctx, clientID, req.DeviceCode, exchanger) if err != nil { return err } @@ -243,11 +226,6 @@ func deviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchang WithDescription("confidential client requires authentication") } - tokenRequest := &deviceAccessTokenRequest{ - subject: state.Subject, - audience: []string{clientID}, - scopes: state.Scopes, - } resp, err := CreateDeviceTokenResponse(r.Context(), tokenRequest, exchanger, client) if err != nil { return err @@ -265,6 +243,50 @@ func ParseDeviceAccessTokenRequest(r *http.Request, exchanger Exchanger) (*oidc. return req, nil } +// DeviceAuthorizationState describes the current state of +// the device authorization flow. +// It implements the [IDTokenRequest] interface. +type DeviceAuthorizationState struct { + ClientID string + Audience []string + Scopes []string + Expires time.Time // The time after we consider the authorization request timed-out + Done bool // The user authenticated and approved the authorization request + Denied bool // The user authenticated and denied the authorization request + + // The following fields are populated after Done == true + Subject string + AMR []string + AuthTime time.Time +} + +func (r *DeviceAuthorizationState) GetAMR() []string { + return r.AMR +} + +func (r *DeviceAuthorizationState) GetAudience() []string { + if !strs.Contains(r.Audience, r.ClientID) { + r.Audience = append(r.Audience, r.ClientID) + } + return r.Audience +} + +func (r *DeviceAuthorizationState) GetAuthTime() time.Time { + return r.AuthTime +} + +func (r *DeviceAuthorizationState) GetClientID() string { + return r.ClientID +} + +func (r *DeviceAuthorizationState) GetScopes() []string { + return r.Scopes +} + +func (r *DeviceAuthorizationState) GetSubject() string { + return r.Subject +} + func CheckDeviceAuthorizationState(ctx context.Context, clientID, deviceCode string, exchanger Exchanger) (*DeviceAuthorizationState, error) { storage, err := assertDeviceStorage(exchanger.Storage()) if err != nil { @@ -291,15 +313,32 @@ func CheckDeviceAuthorizationState(ctx context.Context, clientID, deviceCode str } func CreateDeviceTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator, client Client) (*oidc.AccessTokenResponse, error) { + /* TODO(v4): + Change the TokenRequest argument type to *DeviceAuthorizationState. + Breaking change that can not be done for v3. + */ + ctx, span := tracer.Start(ctx, "CreateDeviceTokenResponse") + defer span.End() + accessToken, refreshToken, validity, err := CreateAccessToken(ctx, tokenRequest, client.AccessTokenType(), creator, client, "") if err != nil { return nil, err } - return &oidc.AccessTokenResponse{ + response := &oidc.AccessTokenResponse{ AccessToken: accessToken, RefreshToken: refreshToken, TokenType: oidc.BearerToken, ExpiresIn: uint64(validity.Seconds()), - }, nil + } + + // TODO(v4): remove type assertion + if idTokenRequest, ok := tokenRequest.(IDTokenRequest); ok && strs.Contains(tokenRequest.GetScopes(), oidc.ScopeOpenID) { + response.IDToken, err = CreateIDToken(ctx, IssuerFromContext(ctx), idTokenRequest, client.IDTokenLifetime(), accessToken, "", creator.Storage(), client) + if err != nil { + return nil, err + } + } + + return response, nil } diff --git a/pkg/op/device_test.go b/pkg/op/device_test.go index f5452f9..570b943 100644 --- a/pkg/op/device_test.go +++ b/pkg/op/device_test.go @@ -453,3 +453,96 @@ func TestCheckDeviceAuthorizationState(t *testing.T) { }) } } + +func TestCreateDeviceTokenResponse(t *testing.T) { + tests := []struct { + name string + tokenRequest op.TokenRequest + wantAccessToken bool + wantRefreshToken bool + wantIDToken bool + wantErr bool + }{ + { + name: "access token", + tokenRequest: &op.DeviceAuthorizationState{ + ClientID: "client1", + Subject: "id1", + AMR: []string{"password"}, + AuthTime: time.Now(), + }, + wantAccessToken: true, + }, + { + name: "access and refresh tokens", + tokenRequest: &op.DeviceAuthorizationState{ + ClientID: "client1", + Subject: "id1", + AMR: []string{"password"}, + AuthTime: time.Now(), + Scopes: []string{oidc.ScopeOfflineAccess}, + }, + wantAccessToken: true, + wantRefreshToken: true, + }, + { + name: "access and id token", + tokenRequest: &op.DeviceAuthorizationState{ + ClientID: "client1", + Subject: "id1", + AMR: []string{"password"}, + AuthTime: time.Now(), + Scopes: []string{oidc.ScopeOpenID}, + }, + wantAccessToken: true, + wantIDToken: true, + }, + { + name: "access, refresh and id token", + tokenRequest: &op.DeviceAuthorizationState{ + ClientID: "client1", + Subject: "id1", + AMR: []string{"password"}, + AuthTime: time.Now(), + Scopes: []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID}, + }, + wantAccessToken: true, + wantRefreshToken: true, + wantIDToken: true, + }, + { + name: "id token creation error", + tokenRequest: &op.DeviceAuthorizationState{ + ClientID: "client1", + Subject: "foobar", + AMR: []string{"password"}, + AuthTime: time.Now(), + Scopes: []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := testProvider.Storage().GetClientByClientID(context.Background(), "native") + require.NoError(t, err) + + got, err := op.CreateDeviceTokenResponse(context.Background(), tt.tokenRequest, testProvider, client) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.InDelta(t, 300, got.ExpiresIn, 2) + if tt.wantAccessToken { + assert.NotEmpty(t, got.AccessToken, "access token") + } + if tt.wantRefreshToken { + assert.NotEmpty(t, got.RefreshToken, "refresh token") + } + if tt.wantIDToken { + assert.NotEmpty(t, got.IDToken, "id token") + } + }) + } +} diff --git a/pkg/op/server_legacy.go b/pkg/op/server_legacy.go index a851a2a..114d431 100644 --- a/pkg/op/server_legacy.go +++ b/pkg/op/server_legacy.go @@ -291,7 +291,7 @@ func (s *LegacyServer) ClientCredentialsExchange(ctx context.Context, r *ClientR } func (s *LegacyServer) DeviceToken(ctx context.Context, r *ClientRequest[oidc.DeviceAccessTokenRequest]) (*Response, error) { - if !s.provider.GrantTypeClientCredentialsSupported() { + if !s.provider.GrantTypeDeviceCodeSupported() { return nil, unimplementedGrantError(oidc.GrantTypeDeviceCode) } // use a limited context timeout shorter as the default @@ -299,15 +299,10 @@ func (s *LegacyServer) DeviceToken(ctx context.Context, r *ClientRequest[oidc.De ctx, cancel := context.WithTimeout(ctx, 4*time.Second) defer cancel() - state, err := CheckDeviceAuthorizationState(ctx, r.Client.GetID(), r.Data.DeviceCode, s.provider) + tokenRequest, err := CheckDeviceAuthorizationState(ctx, r.Client.GetID(), r.Data.DeviceCode, s.provider) if err != nil { return nil, err } - tokenRequest := &deviceAccessTokenRequest{ - subject: state.Subject, - audience: []string{r.Client.GetID()}, - scopes: state.Scopes, - } resp, err := CreateDeviceTokenResponse(ctx, tokenRequest, s.provider, r.Client) if err != nil { return nil, err diff --git a/pkg/op/storage.go b/pkg/op/storage.go index d083a31..a1a00ed 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -168,15 +168,6 @@ type EndSessionRequest struct { var ErrDuplicateUserCode = errors.New("user code already exists") -type DeviceAuthorizationState struct { - ClientID string - Scopes []string - Expires time.Time - Done bool - Subject string - Denied bool -} - type DeviceAuthorizationStorage interface { // StoreDeviceAuthorizationRequest stores a new device authorization request in the database. // User code will be used by the user to complete the login flow and must be unique. diff --git a/pkg/op/token.go b/pkg/op/token.go index 63a01a6..83889f0 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -84,6 +84,8 @@ func needsRefreshToken(tokenRequest TokenRequest, client AccessTokenClient) bool return req.GetRequestedTokenType() == oidc.RefreshTokenType case RefreshTokenRequest: return true + case *DeviceAuthorizationState: + return strings.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && ValidateGrantType(client, oidc.GrantTypeRefreshToken) default: return false } From e6d41bdd5d39cacdf444b6d1425e209be36633a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Dec 2023 11:54:35 +0200 Subject: [PATCH 322/502] chore(deps): bump github/codeql-action from 2 to 3 (#501) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v2...v3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a8106ae..27fa244 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -29,7 +29,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 # Override language selection by uncommenting this and choosing your languages with: languages: go @@ -37,7 +37,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -51,4 +51,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From 2b35eeb83560d69c6b492eb5dcad02ad30c6da55 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Dec 2023 12:15:36 +0200 Subject: [PATCH 323/502] chore(deps): bump golang.org/x/crypto from 0.16.0 to 0.17.0 (#502) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.16.0 to 0.17.0. - [Commits](https://github.com/golang/crypto/compare/v0.16.0...v0.17.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 62d42a5..57ea200 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/otel/metric v1.21.0 // indirect - golang.org/x/crypto v0.16.0 // indirect + golang.org/x/crypto v0.17.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/sys v0.15.0 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index 8cff8d2..26e2b66 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= From 6a8e144e8d37feb32ef3a412ad50a3dd08984e9f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Dec 2023 18:26:55 +0200 Subject: [PATCH 324/502] chore(deps): bump github.com/go-chi/chi/v5 from 5.0.10 to 5.0.11 (#504) Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.0.10 to 5.0.11. - [Release notes](https://github.com/go-chi/chi/releases) - [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md) - [Commits](https://github.com/go-chi/chi/compare/v5.0.10...v5.0.11) --- updated-dependencies: - dependency-name: github.com/go-chi/chi/v5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 57ea200..29bc0ee 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/zitadel/oidc/v3 go 1.19 require ( - github.com/go-chi/chi/v5 v5.0.10 + github.com/go-chi/chi/v5 v5.0.11 github.com/go-jose/go-jose/v3 v3.0.1 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 diff --git a/go.sum b/go.sum index 26e2b66..6b643d9 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ 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/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= -github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= +github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= From dce79a73fb3db57e90b60e0ff409ae8bd8fb496e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 22 Dec 2023 11:25:58 +0200 Subject: [PATCH 325/502] fix(oidc): ignore unknown language tag in userinfo unmarshal (#505) * fix(oidc): ignore unknown language tag in userinfo unmarshal Open system reported an issue where a generic OpenID provider might return language tags like "gb". These tags are well-formed but unknown and Go returns an error for it. We already ignored unknown tags is ui_locale arrays lik in AuthRequest. This change ignores singular unknown tags, like used in the userinfo `locale` claim. * do not set nil to Locale field --- pkg/oidc/types.go | 18 ++++++++++++++++- pkg/oidc/types_test.go | 46 +++++++++++++++++++++++++++++++++--------- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index d8372b8..0e7152c 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -3,6 +3,7 @@ package oidc import ( "database/sql/driver" "encoding/json" + "errors" "fmt" "reflect" "strings" @@ -76,8 +77,23 @@ func (l *Locale) MarshalJSON() ([]byte, error) { return json.Marshal(tag) } +// UnmarshalJSON implements json.Unmarshaler. +// When [language.ValueError] is encountered, the containing tag will be set +// to an empty value (language "und") and no error will be returned. +// This state can be checked with the `l.Tag().IsRoot()` method. func (l *Locale) UnmarshalJSON(data []byte) error { - return json.Unmarshal(data, &l.tag) + err := json.Unmarshal(data, &l.tag) + if err == nil { + return nil + } + + // catch "well-formed but unknown" errors + var target language.ValueError + if errors.As(err, &target) { + l.tag = language.Tag{} + return nil + } + return err } type Locales []language.Tag diff --git a/pkg/oidc/types_test.go b/pkg/oidc/types_test.go index af4f113..df93a73 100644 --- a/pkg/oidc/types_test.go +++ b/pkg/oidc/types_test.go @@ -208,20 +208,46 @@ func TestLocale_MarshalJSON(t *testing.T) { } func TestLocale_UnmarshalJSON(t *testing.T) { - type a struct { + type dst struct { Locale *Locale `json:"locale,omitempty"` } - want := a{ - Locale: NewLocale(language.Afrikaans), + tests := []struct { + name string + input string + want dst + wantErr bool + }{ + { + name: "afrikaans, ok", + input: `{"locale": "af"}`, + want: dst{ + Locale: NewLocale(language.Afrikaans), + }, + }, + { + name: "gb, ignored", + input: `{"locale": "gb"}`, + want: dst{ + Locale: &Locale{}, + }, + }, + { + name: "bad form, error", + input: `{"locale": "g!!!!!"}`, + wantErr: true, + }, } - const input = `{"locale": "af"}` - var got a - - require.NoError(t, - json.Unmarshal([]byte(input), &got), - ) - assert.Equal(t, want, got) + for _, tt := range tests { + var got dst + err := json.Unmarshal([]byte(tt.input), &got) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } } func TestParseLocales(t *testing.T) { From c37ca25220936dec4c7e67cfa9be7bf2f9b3b961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 5 Jan 2024 17:30:17 +0200 Subject: [PATCH 326/502] feat(op): allow double star globs (#507) Related to https://github.com/zitadel/zitadel/issues/5110 --- go.mod | 1 + go.sum | 2 + pkg/op/auth_request.go | 4 +- pkg/op/auth_request_test.go | 54 +++++++ pkg/op/client.go | 1 + pkg/op/mock/generate.go | 1 + pkg/op/mock/glob.go | 24 +++ pkg/op/mock/glob.mock.go | 289 ++++++++++++++++++++++++++++++++++++ 8 files changed, 374 insertions(+), 2 deletions(-) create mode 100644 pkg/op/mock/glob.go create mode 100644 pkg/op/mock/glob.mock.go diff --git a/go.mod b/go.mod index 29bc0ee..4934640 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/zitadel/oidc/v3 go 1.19 require ( + github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/go-chi/chi/v5 v5.0.11 github.com/go-jose/go-jose/v3 v3.0.1 github.com/golang/mock v1.6.0 diff --git a/go.sum b/go.sum index 6b643d9..6c0eace 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 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= diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 7ef06a8..02c820e 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -7,10 +7,10 @@ import ( "net" "net/http" "net/url" - "path" "strings" "time" + "github.com/bmatcuk/doublestar/v4" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" str "github.com/zitadel/oidc/v3/pkg/strings" @@ -283,7 +283,7 @@ func checkURIAgainstRedirects(client Client, uri string) error { } if globClient, ok := client.(HasRedirectGlobs); ok { for _, uriGlob := range globClient.RedirectURIGlobs() { - isMatch, err := path.Match(uriGlob, uri) + isMatch, err := doublestar.Match(uriGlob, uri) if err != nil { return oidc.ErrServerError().WithParent(err) } diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index db70fd7..18880f0 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -583,6 +583,60 @@ func TestValidateAuthReqRedirectURI(t *testing.T) { }, false, }, + { + "code flow dev mode has redirect globs regular ok", + args{ + "http://registered.com/callback", + mock.NewHasRedirectGlobsWithConfig(t, []string{"http://registered.com/*"}, op.ApplicationTypeUserAgent, nil, true), + oidc.ResponseTypeCode, + }, + false, + }, + { + "code flow dev mode has redirect globs wildcard ok", + args{ + "http://registered.com/callback", + mock.NewHasRedirectGlobsWithConfig(t, []string{"http://registered.com/*"}, op.ApplicationTypeUserAgent, nil, true), + oidc.ResponseTypeCode, + }, + false, + }, + { + "code flow dev mode has redirect globs double star ok", + args{ + "http://registered.com/callback", + mock.NewHasRedirectGlobsWithConfig(t, []string{"http://**/*"}, op.ApplicationTypeUserAgent, nil, true), + oidc.ResponseTypeCode, + }, + false, + }, + { + "code flow dev mode has redirect globs double star ok", + args{ + "http://registered.com/callback", + mock.NewHasRedirectGlobsWithConfig(t, []string{"http://**/*"}, op.ApplicationTypeUserAgent, nil, true), + oidc.ResponseTypeCode, + }, + false, + }, + { + "code flow dev mode has redirect globs IPv6 ok", + args{ + "http://[::1]:80/callback", + mock.NewHasRedirectGlobsWithConfig(t, []string{"http://\\[::1\\]:80/*"}, op.ApplicationTypeUserAgent, nil, true), + oidc.ResponseTypeCode, + }, + false, + }, + { + "code flow dev mode has redirect globs bad pattern", + args{ + "http://registered.com/callback", + mock.NewHasRedirectGlobsWithConfig(t, []string{"http://**/\\"}, op.ApplicationTypeUserAgent, nil, true), + oidc.ResponseTypeCode, + }, + true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/op/client.go b/pkg/op/client.go index 04ef3c7..0574afa 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -63,6 +63,7 @@ type Client interface { // such as DevMode for the client being enabled. // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest type HasRedirectGlobs interface { + Client RedirectURIGlobs() []string PostLogoutRedirectURIGlobs() []string } diff --git a/pkg/op/mock/generate.go b/pkg/op/mock/generate.go index 590356c..e5cab3e 100644 --- a/pkg/op/mock/generate.go +++ b/pkg/op/mock/generate.go @@ -4,6 +4,7 @@ package mock //go:generate mockgen -package mock -destination ./storage.mock.go github.com/zitadel/oidc/v3/pkg/op Storage //go:generate mockgen -package mock -destination ./authorizer.mock.go github.com/zitadel/oidc/v3/pkg/op Authorizer //go:generate mockgen -package mock -destination ./client.mock.go github.com/zitadel/oidc/v3/pkg/op Client +//go:generate mockgen -package mock -destination ./glob.mock.go github.com/zitadel/oidc/v3/pkg/op HasRedirectGlobs //go:generate mockgen -package mock -destination ./configuration.mock.go github.com/zitadel/oidc/v3/pkg/op Configuration //go:generate mockgen -package mock -destination ./discovery.mock.go github.com/zitadel/oidc/v3/pkg/op DiscoverStorage //go:generate mockgen -package mock -destination ./signer.mock.go github.com/zitadel/oidc/v3/pkg/op SigningKey,Key diff --git a/pkg/op/mock/glob.go b/pkg/op/mock/glob.go new file mode 100644 index 0000000..cade476 --- /dev/null +++ b/pkg/op/mock/glob.go @@ -0,0 +1,24 @@ +package mock + +import ( + "testing" + + gomock "github.com/golang/mock/gomock" + "github.com/zitadel/oidc/v3/pkg/oidc" + op "github.com/zitadel/oidc/v3/pkg/op" +) + +func NewHasRedirectGlobs(t *testing.T) op.HasRedirectGlobs { + return NewMockHasRedirectGlobs(gomock.NewController(t)) +} + +func NewHasRedirectGlobsWithConfig(t *testing.T, uri []string, appType op.ApplicationType, responseTypes []oidc.ResponseType, devMode bool) op.HasRedirectGlobs { + c := NewHasRedirectGlobs(t) + m := c.(*MockHasRedirectGlobs) + m.EXPECT().RedirectURIs().AnyTimes().Return(uri) + m.EXPECT().RedirectURIGlobs().AnyTimes().Return(uri) + m.EXPECT().ApplicationType().AnyTimes().Return(appType) + m.EXPECT().ResponseTypes().AnyTimes().Return(responseTypes) + m.EXPECT().DevMode().AnyTimes().Return(devMode) + return c +} diff --git a/pkg/op/mock/glob.mock.go b/pkg/op/mock/glob.mock.go new file mode 100644 index 0000000..cf9996e --- /dev/null +++ b/pkg/op/mock/glob.mock.go @@ -0,0 +1,289 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/zitadel/oidc/v3/pkg/op (interfaces: HasRedirectGlobs) + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + time "time" + + gomock "github.com/golang/mock/gomock" + oidc "github.com/zitadel/oidc/v3/pkg/oidc" + op "github.com/zitadel/oidc/v3/pkg/op" +) + +// MockHasRedirectGlobs is a mock of HasRedirectGlobs interface. +type MockHasRedirectGlobs struct { + ctrl *gomock.Controller + recorder *MockHasRedirectGlobsMockRecorder +} + +// MockHasRedirectGlobsMockRecorder is the mock recorder for MockHasRedirectGlobs. +type MockHasRedirectGlobsMockRecorder struct { + mock *MockHasRedirectGlobs +} + +// NewMockHasRedirectGlobs creates a new mock instance. +func NewMockHasRedirectGlobs(ctrl *gomock.Controller) *MockHasRedirectGlobs { + mock := &MockHasRedirectGlobs{ctrl: ctrl} + mock.recorder = &MockHasRedirectGlobsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHasRedirectGlobs) EXPECT() *MockHasRedirectGlobsMockRecorder { + return m.recorder +} + +// AccessTokenType mocks base method. +func (m *MockHasRedirectGlobs) AccessTokenType() op.AccessTokenType { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AccessTokenType") + ret0, _ := ret[0].(op.AccessTokenType) + return ret0 +} + +// AccessTokenType indicates an expected call of AccessTokenType. +func (mr *MockHasRedirectGlobsMockRecorder) AccessTokenType() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccessTokenType", reflect.TypeOf((*MockHasRedirectGlobs)(nil).AccessTokenType)) +} + +// ApplicationType mocks base method. +func (m *MockHasRedirectGlobs) ApplicationType() op.ApplicationType { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ApplicationType") + ret0, _ := ret[0].(op.ApplicationType) + return ret0 +} + +// ApplicationType indicates an expected call of ApplicationType. +func (mr *MockHasRedirectGlobsMockRecorder) ApplicationType() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplicationType", reflect.TypeOf((*MockHasRedirectGlobs)(nil).ApplicationType)) +} + +// AuthMethod mocks base method. +func (m *MockHasRedirectGlobs) AuthMethod() oidc.AuthMethod { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AuthMethod") + ret0, _ := ret[0].(oidc.AuthMethod) + return ret0 +} + +// AuthMethod indicates an expected call of AuthMethod. +func (mr *MockHasRedirectGlobsMockRecorder) AuthMethod() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthMethod", reflect.TypeOf((*MockHasRedirectGlobs)(nil).AuthMethod)) +} + +// ClockSkew mocks base method. +func (m *MockHasRedirectGlobs) ClockSkew() time.Duration { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ClockSkew") + ret0, _ := ret[0].(time.Duration) + return ret0 +} + +// ClockSkew indicates an expected call of ClockSkew. +func (mr *MockHasRedirectGlobsMockRecorder) ClockSkew() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClockSkew", reflect.TypeOf((*MockHasRedirectGlobs)(nil).ClockSkew)) +} + +// DevMode mocks base method. +func (m *MockHasRedirectGlobs) DevMode() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DevMode") + ret0, _ := ret[0].(bool) + return ret0 +} + +// DevMode indicates an expected call of DevMode. +func (mr *MockHasRedirectGlobsMockRecorder) DevMode() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DevMode", reflect.TypeOf((*MockHasRedirectGlobs)(nil).DevMode)) +} + +// GetID mocks base method. +func (m *MockHasRedirectGlobs) GetID() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetID") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetID indicates an expected call of GetID. +func (mr *MockHasRedirectGlobsMockRecorder) GetID() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetID", reflect.TypeOf((*MockHasRedirectGlobs)(nil).GetID)) +} + +// GrantTypes mocks base method. +func (m *MockHasRedirectGlobs) GrantTypes() []oidc.GrantType { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GrantTypes") + ret0, _ := ret[0].([]oidc.GrantType) + return ret0 +} + +// GrantTypes indicates an expected call of GrantTypes. +func (mr *MockHasRedirectGlobsMockRecorder) GrantTypes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypes", reflect.TypeOf((*MockHasRedirectGlobs)(nil).GrantTypes)) +} + +// IDTokenLifetime mocks base method. +func (m *MockHasRedirectGlobs) IDTokenLifetime() time.Duration { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IDTokenLifetime") + ret0, _ := ret[0].(time.Duration) + return ret0 +} + +// IDTokenLifetime indicates an expected call of IDTokenLifetime. +func (mr *MockHasRedirectGlobsMockRecorder) IDTokenLifetime() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenLifetime", reflect.TypeOf((*MockHasRedirectGlobs)(nil).IDTokenLifetime)) +} + +// IDTokenUserinfoClaimsAssertion mocks base method. +func (m *MockHasRedirectGlobs) IDTokenUserinfoClaimsAssertion() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IDTokenUserinfoClaimsAssertion") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IDTokenUserinfoClaimsAssertion indicates an expected call of IDTokenUserinfoClaimsAssertion. +func (mr *MockHasRedirectGlobsMockRecorder) IDTokenUserinfoClaimsAssertion() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenUserinfoClaimsAssertion", reflect.TypeOf((*MockHasRedirectGlobs)(nil).IDTokenUserinfoClaimsAssertion)) +} + +// IsScopeAllowed mocks base method. +func (m *MockHasRedirectGlobs) IsScopeAllowed(arg0 string) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsScopeAllowed", arg0) + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsScopeAllowed indicates an expected call of IsScopeAllowed. +func (mr *MockHasRedirectGlobsMockRecorder) IsScopeAllowed(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsScopeAllowed", reflect.TypeOf((*MockHasRedirectGlobs)(nil).IsScopeAllowed), arg0) +} + +// LoginURL mocks base method. +func (m *MockHasRedirectGlobs) LoginURL(arg0 string) string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LoginURL", arg0) + ret0, _ := ret[0].(string) + return ret0 +} + +// LoginURL indicates an expected call of LoginURL. +func (mr *MockHasRedirectGlobsMockRecorder) LoginURL(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoginURL", reflect.TypeOf((*MockHasRedirectGlobs)(nil).LoginURL), arg0) +} + +// PostLogoutRedirectURIGlobs mocks base method. +func (m *MockHasRedirectGlobs) PostLogoutRedirectURIGlobs() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PostLogoutRedirectURIGlobs") + ret0, _ := ret[0].([]string) + return ret0 +} + +// PostLogoutRedirectURIGlobs indicates an expected call of PostLogoutRedirectURIGlobs. +func (mr *MockHasRedirectGlobsMockRecorder) PostLogoutRedirectURIGlobs() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostLogoutRedirectURIGlobs", reflect.TypeOf((*MockHasRedirectGlobs)(nil).PostLogoutRedirectURIGlobs)) +} + +// PostLogoutRedirectURIs mocks base method. +func (m *MockHasRedirectGlobs) PostLogoutRedirectURIs() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PostLogoutRedirectURIs") + ret0, _ := ret[0].([]string) + return ret0 +} + +// PostLogoutRedirectURIs indicates an expected call of PostLogoutRedirectURIs. +func (mr *MockHasRedirectGlobsMockRecorder) PostLogoutRedirectURIs() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostLogoutRedirectURIs", reflect.TypeOf((*MockHasRedirectGlobs)(nil).PostLogoutRedirectURIs)) +} + +// RedirectURIGlobs mocks base method. +func (m *MockHasRedirectGlobs) RedirectURIGlobs() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RedirectURIGlobs") + ret0, _ := ret[0].([]string) + return ret0 +} + +// RedirectURIGlobs indicates an expected call of RedirectURIGlobs. +func (mr *MockHasRedirectGlobsMockRecorder) RedirectURIGlobs() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RedirectURIGlobs", reflect.TypeOf((*MockHasRedirectGlobs)(nil).RedirectURIGlobs)) +} + +// RedirectURIs mocks base method. +func (m *MockHasRedirectGlobs) RedirectURIs() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RedirectURIs") + ret0, _ := ret[0].([]string) + return ret0 +} + +// RedirectURIs indicates an expected call of RedirectURIs. +func (mr *MockHasRedirectGlobsMockRecorder) RedirectURIs() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RedirectURIs", reflect.TypeOf((*MockHasRedirectGlobs)(nil).RedirectURIs)) +} + +// ResponseTypes mocks base method. +func (m *MockHasRedirectGlobs) ResponseTypes() []oidc.ResponseType { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ResponseTypes") + ret0, _ := ret[0].([]oidc.ResponseType) + return ret0 +} + +// ResponseTypes indicates an expected call of ResponseTypes. +func (mr *MockHasRedirectGlobsMockRecorder) ResponseTypes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResponseTypes", reflect.TypeOf((*MockHasRedirectGlobs)(nil).ResponseTypes)) +} + +// RestrictAdditionalAccessTokenScopes mocks base method. +func (m *MockHasRedirectGlobs) RestrictAdditionalAccessTokenScopes() func([]string) []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RestrictAdditionalAccessTokenScopes") + ret0, _ := ret[0].(func([]string) []string) + return ret0 +} + +// RestrictAdditionalAccessTokenScopes indicates an expected call of RestrictAdditionalAccessTokenScopes. +func (mr *MockHasRedirectGlobsMockRecorder) RestrictAdditionalAccessTokenScopes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestrictAdditionalAccessTokenScopes", reflect.TypeOf((*MockHasRedirectGlobs)(nil).RestrictAdditionalAccessTokenScopes)) +} + +// RestrictAdditionalIdTokenScopes mocks base method. +func (m *MockHasRedirectGlobs) RestrictAdditionalIdTokenScopes() func([]string) []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RestrictAdditionalIdTokenScopes") + ret0, _ := ret[0].(func([]string) []string) + return ret0 +} + +// RestrictAdditionalIdTokenScopes indicates an expected call of RestrictAdditionalIdTokenScopes. +func (mr *MockHasRedirectGlobsMockRecorder) RestrictAdditionalIdTokenScopes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestrictAdditionalIdTokenScopes", reflect.TypeOf((*MockHasRedirectGlobs)(nil).RestrictAdditionalIdTokenScopes)) +} From e23b1d475435b695c3e33baac204d5a47ecf55f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Mon, 8 Jan 2024 09:01:34 +0100 Subject: [PATCH 327/502] fix: Implement dedicated error for RevokeToken (#508) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan-Otto Kröpke --- pkg/client/rp/errors.go | 5 +++++ pkg/client/rp/relying_party.go | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 pkg/client/rp/errors.go diff --git a/pkg/client/rp/errors.go b/pkg/client/rp/errors.go new file mode 100644 index 0000000..b95420b --- /dev/null +++ b/pkg/client/rp/errors.go @@ -0,0 +1,5 @@ +package rp + +import "errors" + +var ErrRelyingPartyNotSupportRevokeCaller = errors.New("RelyingParty does not support RevokeCaller") diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 5899af0..e31b025 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -4,12 +4,11 @@ import ( "context" "encoding/base64" "errors" - "fmt" "net/http" "net/url" "time" - jose "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v3" "github.com/google/uuid" "github.com/zitadel/logging" "golang.org/x/exp/slog" @@ -726,5 +725,5 @@ func RevokeToken(ctx context.Context, rp RelyingParty, token string, tokenTypeHi if rc, ok := rp.(client.RevokeCaller); ok && rc.GetRevokeEndpoint() != "" { return client.CallRevokeEndpoint(ctx, request, nil, rc) } - return fmt.Errorf("RelyingParty does not support RevokeCaller") + return ErrRelyingPartyNotSupportRevokeCaller } From 8923b821427f0a7537778791c79568a119b82fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Mon, 8 Jan 2024 11:18:33 +0200 Subject: [PATCH 328/502] chore(deps): enable dependabot for the v2 branch (#512) --- .github/dependabot.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 79ff704..1efdcf8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,6 +9,16 @@ updates: commit-message: prefix: chore include: scope +- package-ecosystem: gomod + target-branch: "2.12.x" + directory: "/" + schedule: + interval: daily + time: '04:00' + open-pull-requests-limit: 10 + commit-message: + prefix: chore + include: scope - package-ecosystem: "github-actions" directory: "/" schedule: From 4d85375702dca1a96d6b835d39424f8a3112525e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Mon, 8 Jan 2024 11:21:28 +0200 Subject: [PATCH 329/502] chore(example): add device package level documentation (#510) --- example/client/device/device.go | 36 ++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/example/client/device/device.go b/example/client/device/device.go index bea6134..78ed2c8 100644 --- a/example/client/device/device.go +++ b/example/client/device/device.go @@ -1,3 +1,37 @@ +// Command device is an example Oauth2 Device Authorization Grant app. +// It creates a new Device Authorization request on the Issuer and then polls for tokens. +// The user is then prompted to visit a URL and enter the user code. +// Or, the complete URL can be used instead to omit manual entry. +// In practice then can be a "magic link" in the form or a QR. +// +// The following environment variables are used for configuration: +// +// ISSUER: URL to the OP, required. +// CLIENT_ID: ID of the application, required. +// CLIENT_SECRET: Secret to authenticate the app using basic auth. Only required if the OP expects this type of authentication. +// KEY_PATH: Path to a private key file, used to for JWT authentication of the App. Only required if the OP expects this type of authentication. +// SCOPES: Scopes of the Authentication Request. Optional. +// +// Basic usage: +// +// cd example/client/device +// export ISSUER="http://localhost:9000" CLIENT_ID="246048465824634593@demo" +// +// Get an Access Token: +// +// SCOPES="email profile" go run . +// +// Get an Access Token and ID Token: +// +// SCOPES="email profile openid" go run . +// +// Get an Access Token and Refresh Token +// +// SCOPES="email profile offline_access" go run . +// +// Get Access, Refresh and ID Tokens: +// +// SCOPES="email profile offline_access openid" go run . package main import ( @@ -57,5 +91,5 @@ func main() { if err != nil { logrus.Fatal(err) } - logrus.Infof("successfully obtained token: %v", token) + logrus.Infof("successfully obtained token: %#v", token) } From 5dcf6de055a5da59d8a688d57e8384428640194d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jan 2024 17:17:04 +0200 Subject: [PATCH 330/502] chore(deps): bump golang.org/x/oauth2 from 0.15.0 to 0.16.0 (#513) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.15.0 to 0.16.0. - [Commits](https://github.com/golang/oauth2/compare/v0.15.0...v0.16.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 4934640..737fa45 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( go.opentelemetry.io/otel v1.21.0 go.opentelemetry.io/otel/trace v1.21.0 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 - golang.org/x/oauth2 v0.15.0 + golang.org/x/oauth2 v0.16.0 golang.org/x/text v0.14.0 ) @@ -33,9 +33,9 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/otel/metric v1.21.0 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 6c0eace..20e9734 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -77,11 +77,11 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= -golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -90,8 +90,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= From 984e31a9e22e8b43a7ac6bfaeab29db56b28f69f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Tue, 9 Jan 2024 16:24:05 +0100 Subject: [PATCH 331/502] feat(rp): Add UnauthorizedHandler (#503) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * RP: Add UnauthorizedHandler Signed-off-by: Jan-Otto Kröpke * remove race condition Signed-off-by: Jan-Otto Kröpke * Use optional interface Signed-off-by: Jan-Otto Kröpke --------- Signed-off-by: Jan-Otto Kröpke --- pkg/client/rp/relying_party.go | 62 +++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index e31b025..04be4ef 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -66,19 +66,28 @@ type RelyingParty interface { // IDTokenVerifier returns the verifier used for oidc id_token verification IDTokenVerifier() *IDTokenVerifier - // ErrorHandler returns the handler used for callback errors + // ErrorHandler returns the handler used for callback errors ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) // Logger from the context, or a fallback if set. Logger(context.Context) (logger *slog.Logger, ok bool) } +type HasUnauthorizedHandler interface { + // UnauthorizedHandler returns the handler used for unauthorized errors + UnauthorizedHandler() func(w http.ResponseWriter, r *http.Request, desc string, state string) +} + type ErrorHandler func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) +type UnauthorizedHandler func(w http.ResponseWriter, r *http.Request, desc string, state string) var DefaultErrorHandler ErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) { http.Error(w, errorType+": "+errorDesc, http.StatusInternalServerError) } +var DefaultUnauthorizedHandler UnauthorizedHandler = func(w http.ResponseWriter, r *http.Request, desc string, state string) { + http.Error(w, desc, http.StatusUnauthorized) +} type relyingParty struct { issuer string @@ -91,11 +100,12 @@ type relyingParty struct { httpClient *http.Client cookieHandler *httphelper.CookieHandler - errorHandler func(http.ResponseWriter, *http.Request, string, string, string) - idTokenVerifier *IDTokenVerifier - verifierOpts []VerifierOption - signer jose.Signer - logger *slog.Logger + errorHandler func(http.ResponseWriter, *http.Request, string, string, string) + unauthorizedHandler func(http.ResponseWriter, *http.Request, string, string) + idTokenVerifier *IDTokenVerifier + verifierOpts []VerifierOption + signer jose.Signer + logger *slog.Logger } func (rp *relyingParty) OAuthConfig() *oauth2.Config { @@ -156,6 +166,10 @@ func (rp *relyingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, return rp.errorHandler } +func (rp *relyingParty) UnauthorizedHandler() func(http.ResponseWriter, *http.Request, string, string) { + return rp.unauthorizedHandler +} + func (rp *relyingParty) Logger(ctx context.Context) (logger *slog.Logger, ok bool) { logger, ok = logging.FromContext(ctx) if ok { @@ -169,9 +183,10 @@ func (rp *relyingParty) Logger(ctx context.Context) (logger *slog.Logger, ok boo // it will use the AuthURL and TokenURL set in config func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingParty, error) { rp := &relyingParty{ - oauthConfig: config, - httpClient: httphelper.DefaultHTTPClient, - oauth2Only: true, + oauthConfig: config, + httpClient: httphelper.DefaultHTTPClient, + oauth2Only: true, + unauthorizedHandler: DefaultUnauthorizedHandler, } for _, optFunc := range options { @@ -268,6 +283,13 @@ func WithErrorHandler(errorHandler ErrorHandler) Option { } } +func WithUnauthorizedHandler(unauthorizedHandler UnauthorizedHandler) Option { + return func(rp *relyingParty) error { + rp.unauthorizedHandler = unauthorizedHandler + return nil + } +} + func WithVerifierOpts(opts ...VerifierOption) Option { return func(rp *relyingParty) error { rp.verifierOpts = opts @@ -355,13 +377,13 @@ func AuthURLHandler(stateFn func() string, rp RelyingParty, urlParam ...URLParam state := stateFn() if err := trySetStateCookie(w, state, rp); err != nil { - http.Error(w, "failed to create state cookie: "+err.Error(), http.StatusUnauthorized) + unauthorizedError(w, r, "failed to create state cookie: "+err.Error(), state, rp) return } if rp.IsPKCE() { codeChallenge, err := GenerateAndStoreCodeChallenge(w, rp) if err != nil { - http.Error(w, "failed to create code challenge: "+err.Error(), http.StatusUnauthorized) + unauthorizedError(w, r, "failed to create code challenge: "+err.Error(), state, rp) return } opts = append(opts, WithCodeChallenge(codeChallenge)) @@ -448,7 +470,7 @@ func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp R return func(w http.ResponseWriter, r *http.Request) { state, err := tryReadStateCookie(w, r, rp) if err != nil { - http.Error(w, "failed to get state: "+err.Error(), http.StatusUnauthorized) + unauthorizedError(w, r, "failed to get state: "+err.Error(), state, rp) return } params := r.URL.Query() @@ -464,7 +486,7 @@ func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp R if rp.IsPKCE() { codeVerifier, err := rp.CookieHandler().CheckCookie(r, pkceCode) if err != nil { - http.Error(w, "failed to get code verifier: "+err.Error(), http.StatusUnauthorized) + unauthorizedError(w, r, "failed to get code verifier: "+err.Error(), state, rp) return } codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier)) @@ -473,14 +495,14 @@ func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp R if rp.Signer() != nil { assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer()}, time.Hour, rp.Signer()) if err != nil { - http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized) + unauthorizedError(w, r, "failed to build assertion: "+err.Error(), state, rp) return } codeOpts = append(codeOpts, WithClientAssertionJWT(assertion)) } tokens, err := CodeExchange[C](r.Context(), params.Get("code"), rp, codeOpts...) if err != nil { - http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) + unauthorizedError(w, r, "failed to exchange token: "+err.Error(), state, rp) return } callback(w, r, tokens, state, rp) @@ -500,7 +522,7 @@ func UserinfoCallback[C oidc.IDClaims, U SubjectGetter](f CodeExchangeUserinfoCa return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) { info, err := Userinfo[U](r.Context(), tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp) if err != nil { - http.Error(w, "userinfo failed: "+err.Error(), http.StatusUnauthorized) + unauthorizedError(w, r, "userinfo failed: "+err.Error(), state, rp) return } f(w, r, tokens, state, rp, info) @@ -727,3 +749,11 @@ func RevokeToken(ctx context.Context, rp RelyingParty, token string, tokenTypeHi } return ErrRelyingPartyNotSupportRevokeCaller } + +func unauthorizedError(w http.ResponseWriter, r *http.Request, desc string, state string, rp RelyingParty) { + if rp, ok := rp.(HasUnauthorizedHandler); ok { + rp.UnauthorizedHandler()(w, r, desc, state) + return + } + http.Error(w, desc, http.StatusUnauthorized) +} From 844e2337bb96a513c559494decafe5d7901d5d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 16 Jan 2024 08:18:41 +0200 Subject: [PATCH 332/502] fix(op): check redirect URI in code exchange (#516) This changes fixes a missing redirect check in the Legacy Server's Code Exchange handler. --- pkg/op/server_legacy.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/op/server_legacy.go b/pkg/op/server_legacy.go index 114d431..089be6f 100644 --- a/pkg/op/server_legacy.go +++ b/pkg/op/server_legacy.go @@ -210,6 +210,9 @@ func (s *LegacyServer) CodeExchange(ctx context.Context, r *ClientRequest[oidc.A return nil, err } } + if r.Data.RedirectURI != authReq.GetRedirectURI() { + return nil, oidc.ErrInvalidGrant().WithDescription("redirect_uri does not correspond") + } resp, err := CreateTokenResponse(ctx, authReq, r.Client, s.provider, true, r.Data.Code, "") if err != nil { return nil, err From 57d04e74651766c47097c8253bf08417a366d553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 17 Jan 2024 17:06:45 +0200 Subject: [PATCH 333/502] fix: don't force server errors in legacy server (#517) * fix: don't force server errors in legacy server * fix tests and be more consistent with the returned status code --- pkg/op/auth_request.go | 10 +++++----- pkg/op/error.go | 32 ++++++++++++++++++++++++-------- pkg/op/error_test.go | 6 +++++- pkg/op/server_http_test.go | 6 +++--- pkg/op/server_legacy.go | 10 +++++----- 5 files changed, 42 insertions(+), 22 deletions(-) diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 02c820e..ed368eb 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -138,20 +138,20 @@ func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage } if requestObject.ClientID != "" && requestObject.ClientID != authReq.ClientID { - return oidc.ErrInvalidRequest() + return oidc.ErrInvalidRequest().WithDescription("missing or wrong client id in request") } if requestObject.ResponseType != "" && requestObject.ResponseType != authReq.ResponseType { - return oidc.ErrInvalidRequest() + return oidc.ErrInvalidRequest().WithDescription("missing or wrong response type in request") } if requestObject.Issuer != requestObject.ClientID { - return oidc.ErrInvalidRequest() + return oidc.ErrInvalidRequest().WithDescription("missing or wrong issuer in request") } if !str.Contains(requestObject.Audience, issuer) { - return oidc.ErrInvalidRequest() + return oidc.ErrInvalidRequest().WithDescription("issuer missing in audience") } keySet := &jwtProfileKeySet{storage: storage, clientID: requestObject.Issuer} if err = oidc.CheckSignature(ctx, authReq.RequestParam, payload, requestObject, nil, keySet); err != nil { - return err + return oidc.ErrInvalidRequest().WithParent(err).WithDescription(err.Error()) } CopyRequestObjectToAuthRequest(authReq, requestObject) return nil diff --git a/pkg/op/error.go b/pkg/op/error.go index 0cac14b..e4580f6 100644 --- a/pkg/op/error.go +++ b/pkg/op/error.go @@ -157,13 +157,29 @@ func (e StatusError) Is(err error) bool { e.statusCode == target.statusCode } -// WriteError asserts for a StatusError containing an [oidc.Error]. -// If no StatusError is found, the status code will default to [http.StatusBadRequest]. -// If no [oidc.Error] was found in the parent, the error type defaults to [oidc.ServerError]. +// WriteError asserts for a [StatusError] containing an [oidc.Error]. +// If no `StatusError` is found, the status code will default to [http.StatusBadRequest]. +// If no `oidc.Error` was found in the parent, the error type defaults to [oidc.ServerError]. +// When there was no `StatusError` and the `oidc.Error` is of type `oidc.ServerError`, +// the status code will be set to [http.StatusInternalServerError] func WriteError(w http.ResponseWriter, r *http.Request, err error, logger *slog.Logger) { - statusError := AsStatusError(err, http.StatusBadRequest) - e := oidc.DefaultToServerError(statusError.parent, statusError.parent.Error()) - - logger.Log(r.Context(), e.LogLevel(), "request error", "oidc_error", e) - httphelper.MarshalJSONWithStatus(w, e, statusError.statusCode) + var statusError StatusError + if errors.As(err, &statusError) { + writeError(w, r, + oidc.DefaultToServerError(statusError.parent, statusError.parent.Error()), + statusError.statusCode, logger, + ) + return + } + statusCode := http.StatusBadRequest + e := oidc.DefaultToServerError(err, err.Error()) + if e.ErrorType == oidc.ServerError { + statusCode = http.StatusInternalServerError + } + writeError(w, r, e, statusCode, logger) +} + +func writeError(w http.ResponseWriter, r *http.Request, err *oidc.Error, statusCode int, logger *slog.Logger) { + logger.Log(r.Context(), err.LogLevel(), "request error", "oidc_error", err, "status_code", statusCode) + httphelper.MarshalJSONWithStatus(w, err, statusCode) } diff --git a/pkg/op/error_test.go b/pkg/op/error_test.go index 689ee5a..50a9cbf 100644 --- a/pkg/op/error_test.go +++ b/pkg/op/error_test.go @@ -579,7 +579,7 @@ func TestWriteError(t *testing.T) { { name: "not a status or oidc error", err: io.ErrClosedPipe, - wantStatus: http.StatusBadRequest, + wantStatus: http.StatusInternalServerError, wantBody: `{ "error":"server_error", "error_description":"io: read/write on closed pipe" @@ -592,6 +592,7 @@ func TestWriteError(t *testing.T) { "parent":"io: read/write on closed pipe", "type":"server_error" }, + "status_code":500, "time":"not" }`, }, @@ -611,6 +612,7 @@ func TestWriteError(t *testing.T) { "parent":"io: read/write on closed pipe", "type":"server_error" }, + "status_code":500, "time":"not" }`, }, @@ -629,6 +631,7 @@ func TestWriteError(t *testing.T) { "description":"oops", "type":"invalid_request" }, + "status_code":400, "time":"not" }`, }, @@ -650,6 +653,7 @@ func TestWriteError(t *testing.T) { "description":"oops", "type":"unauthorized_client" }, + "status_code":401, "time":"not" }`, }, diff --git a/pkg/op/server_http_test.go b/pkg/op/server_http_test.go index 4eac4a0..6cb268f 100644 --- a/pkg/op/server_http_test.go +++ b/pkg/op/server_http_test.go @@ -365,14 +365,14 @@ func Test_webServer_authorizeHandler(t *testing.T) { }, }, { - name: "authorize error", + name: "server error", fields: fields{ server: &requestVerifier{}, decoder: testDecoder, }, r: httptest.NewRequest(http.MethodPost, "/authorize", strings.NewReader("foo=bar")), want: webServerResult{ - wantStatus: http.StatusBadRequest, + wantStatus: http.StatusInternalServerError, wantBody: `{"error":"server_error"}`, }, }, @@ -1237,7 +1237,7 @@ func Test_webServer_simpleHandler(t *testing.T) { }, r: httptest.NewRequest(http.MethodGet, "/", bytes.NewReader(make([]byte, 11<<20))), want: webServerResult{ - wantStatus: http.StatusBadRequest, + wantStatus: http.StatusInternalServerError, wantBody: `{"error":"server_error", "error_description":"io: read/write on closed pipe"}`, }, }, diff --git a/pkg/op/server_legacy.go b/pkg/op/server_legacy.go index 089be6f..f99d15d 100644 --- a/pkg/op/server_legacy.go +++ b/pkg/op/server_legacy.go @@ -91,7 +91,7 @@ func (s *LegacyServer) Ready(ctx context.Context, r *Request[struct{}]) (*Respon for _, probe := range s.provider.Probes() { // shouldn't we run probes in Go routines? if err := probe(ctx); err != nil { - return nil, NewStatusError(err, http.StatusInternalServerError) + return nil, AsStatusError(err, http.StatusInternalServerError) } } return NewResponse(Status{Status: "ok"}), nil @@ -106,7 +106,7 @@ func (s *LegacyServer) Discovery(ctx context.Context, r *Request[struct{}]) (*Re func (s *LegacyServer) Keys(ctx context.Context, r *Request[struct{}]) (*Response, error) { keys, err := s.provider.Storage().KeySet(ctx) if err != nil { - return nil, NewStatusError(err, http.StatusInternalServerError) + return nil, AsStatusError(err, http.StatusInternalServerError) } return NewResponse(jsonWebKeySet(keys)), nil } @@ -127,7 +127,7 @@ func (s *LegacyServer) VerifyAuthRequest(ctx context.Context, r *Request[oidc.Au } } if r.Data.ClientID == "" { - return nil, ErrAuthReqMissingClientID + return nil, oidc.ErrInvalidRequest().WithParent(ErrAuthReqMissingClientID).WithDescription(ErrAuthReqMissingClientID.Error()) } client, err := s.provider.Storage().GetClientByClientID(ctx, r.Data.ClientID) if err != nil { @@ -155,7 +155,7 @@ func (s *LegacyServer) Authorize(ctx context.Context, r *ClientRequest[oidc.Auth func (s *LegacyServer) DeviceAuthorization(ctx context.Context, r *ClientRequest[oidc.DeviceAuthorizationRequest]) (*Response, error) { response, err := createDeviceAuthorization(ctx, r.Data, r.Client.GetID(), s.provider) if err != nil { - return nil, NewStatusError(err, http.StatusInternalServerError) + return nil, AsStatusError(err, http.StatusInternalServerError) } return NewResponse(response), nil } @@ -248,7 +248,7 @@ func (s *LegacyServer) JWTProfile(ctx context.Context, r *Request[oidc.JWTProfil } tokenRequest, err := VerifyJWTAssertion(ctx, r.Data.Assertion, exchanger.JWTProfileVerifier(ctx)) if err != nil { - return nil, err + return nil, oidc.ErrInvalidRequest().WithParent(err).WithDescription("assertion invalid") } tokenRequest.Scopes, err = exchanger.Storage().ValidateJWTProfileScopes(ctx, tokenRequest.Issuer, r.Data.Scope) From 3f26eb10ad45a059900b7c2c0c66f4955fa69101 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jan 2024 12:26:18 +0200 Subject: [PATCH 334/502] chore(deps): bump go.opentelemetry.io/otel/trace from 1.21.0 to 1.22.0 (#520) Bumps [go.opentelemetry.io/otel/trace](https://github.com/open-telemetry/opentelemetry-go) from 1.21.0 to 1.22.0. - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.21.0...v1.22.0) --- updated-dependencies: - dependency-name: go.opentelemetry.io/otel/trace dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 737fa45..45c80e5 100644 --- a/go.mod +++ b/go.mod @@ -18,8 +18,8 @@ require ( github.com/stretchr/testify v1.8.4 github.com/zitadel/logging v0.5.0 github.com/zitadel/schema v1.3.0 - go.opentelemetry.io/otel v1.21.0 - go.opentelemetry.io/otel/trace v1.21.0 + go.opentelemetry.io/otel v1.22.0 + go.opentelemetry.io/otel/trace v1.22.0 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 golang.org/x/oauth2 v0.16.0 golang.org/x/text v0.14.0 @@ -27,12 +27,12 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel/metric v1.21.0 // indirect + go.opentelemetry.io/otel/metric v1.22.0 // indirect golang.org/x/crypto v0.18.0 // indirect golang.org/x/net v0.20.0 // indirect golang.org/x/sys v0.16.0 // indirect diff --git a/go.sum b/go.sum index 20e9734..8080fa9 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNIT github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= -github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= @@ -57,12 +57,12 @@ github.com/zitadel/logging v0.5.0 h1:Kunouvqse/efXy4UDvFw5s3vP+Z4AlHo3y8wF7stXHA github.com/zitadel/logging v0.5.0/go.mod h1:IzP5fzwFhzzyxHkSmfF8dsyqFsQRJLLcQmwhIBzlGsE= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= -go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= -go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= -go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= -go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= -go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= +go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= +go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= +go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= +go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= +go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= From b8e520afd0b8ba0f742145e82d3fb8e1de7ac713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 19 Jan 2024 12:30:51 +0200 Subject: [PATCH 335/502] fix: allow expired ID token hint to end sessions (#522) * fix: allow expired ID token hint to end sessions This change adds a specific error for expired ID Token hints, including too old "issued at" and "max auth age". The error is returned VerifyIDTokenHint so that the end session handler can choose to ignore this error. This fixes the behavior to be in line with [OpenID Connect RP-Initiated Logout 1.0, section 4](https://openid.net/specs/openid-connect-rpinitiated-1_0.html#ValidationAndErrorHandling). * Tes IDTokenHintExpiredError --- pkg/oidc/verifier.go | 2 +- pkg/op/session.go | 3 +- pkg/op/verifier_id_token_hint.go | 36 ++++++++---- pkg/op/verifier_id_token_hint_test.go | 79 +++++++++++++++------------ 4 files changed, 74 insertions(+), 46 deletions(-) diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go index 42fbb20..fe28857 100644 --- a/pkg/oidc/verifier.go +++ b/pkg/oidc/verifier.go @@ -57,7 +57,7 @@ var ( ErrNonceInvalid = errors.New("nonce does not match") ErrAcrInvalid = errors.New("acr is invalid") ErrAuthTimeNotPresent = errors.New("claim `auth_time` of token is missing") - ErrAuthTimeToOld = errors.New("auth time of token is to old") + ErrAuthTimeToOld = errors.New("auth time of token is too old") ErrAtHash = errors.New("at_hash does not correspond to access token") ) diff --git a/pkg/op/session.go b/pkg/op/session.go index c33627f..c933659 100644 --- a/pkg/op/session.go +++ b/pkg/op/session.go @@ -2,6 +2,7 @@ package op import ( "context" + "errors" "net/http" "net/url" "path" @@ -68,7 +69,7 @@ func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest, } if req.IdTokenHint != "" { claims, err := VerifyIDTokenHint[*oidc.IDTokenClaims](ctx, req.IdTokenHint, ender.IDTokenHintVerifier(ctx)) - if err != nil { + if err != nil && !errors.As(err, &IDTokenHintExpiredError{}) { return nil, oidc.ErrInvalidRequest().WithDescription("id_token_hint invalid").WithParent(err) } session.UserID = claims.GetSubject() diff --git a/pkg/op/verifier_id_token_hint.go b/pkg/op/verifier_id_token_hint.go index 6143252..b5ec72e 100644 --- a/pkg/op/verifier_id_token_hint.go +++ b/pkg/op/verifier_id_token_hint.go @@ -2,6 +2,7 @@ package op import ( "context" + "errors" "github.com/zitadel/oidc/v3/pkg/oidc" ) @@ -27,8 +28,23 @@ func NewIDTokenHintVerifier(issuer string, keySet oidc.KeySet, opts ...IDTokenHi return verifier } +type IDTokenHintExpiredError struct { + error +} + +func (e IDTokenHintExpiredError) Unwrap() error { + return e.error +} + +func (e IDTokenHintExpiredError) Is(err error) bool { + return errors.Is(err, e.error) +} + // VerifyIDTokenHint validates the id token according to -// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation +// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation. +// In case of an expired token both the Claims and first encountered expiry related error +// is returned of type [IDTokenHintExpiredError]. In that case the caller can choose to still +// trust the token for cases like logout, as signature and other verifications succeeded. func VerifyIDTokenHint[C oidc.Claims](ctx context.Context, token string, v *IDTokenHintVerifier) (claims C, err error) { var nilClaims C @@ -49,20 +65,20 @@ func VerifyIDTokenHint[C oidc.Claims](ctx context.Context, token string, v *IDTo return nilClaims, err } - if err = oidc.CheckExpiration(claims, v.Offset); err != nil { - return nilClaims, err - } - - if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT, v.Offset); err != nil { - return nilClaims, err - } - if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR); err != nil { return nilClaims, err } + if err = oidc.CheckExpiration(claims, v.Offset); err != nil { + return claims, IDTokenHintExpiredError{err} + } + + if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT, v.Offset); err != nil { + return claims, IDTokenHintExpiredError{err} + } + if err = oidc.CheckAuthTime(claims, v.MaxAge); err != nil { - return nilClaims, err + return claims, IDTokenHintExpiredError{err} } return claims, nil } diff --git a/pkg/op/verifier_id_token_hint_test.go b/pkg/op/verifier_id_token_hint_test.go index e514a76..597e291 100644 --- a/pkg/op/verifier_id_token_hint_test.go +++ b/pkg/op/verifier_id_token_hint_test.go @@ -2,6 +2,7 @@ package op import ( "context" + "errors" "testing" "time" @@ -57,6 +58,13 @@ func TestNewIDTokenHintVerifier(t *testing.T) { } } +func Test_IDTokenHintExpiredError(t *testing.T) { + var err error = IDTokenHintExpiredError{oidc.ErrExpired} + assert.True(t, errors.Unwrap(err) == oidc.ErrExpired) + assert.ErrorIs(t, err, oidc.ErrExpired) + assert.ErrorAs(t, err, &IDTokenHintExpiredError{}) +} + func TestVerifyIDTokenHint(t *testing.T) { verifier := &IDTokenHintVerifier{ Issuer: tu.ValidIssuer, @@ -71,21 +79,23 @@ func TestVerifyIDTokenHint(t *testing.T) { tests := []struct { name string tokenClaims func() (string, *oidc.IDTokenClaims) - wantErr bool + wantClaims bool + wantErr error }{ { name: "success", tokenClaims: tu.ValidIDToken, + wantClaims: true, }, { name: "parse err", tokenClaims: func() (string, *oidc.IDTokenClaims) { return "~~~~", nil }, - wantErr: true, + wantErr: oidc.ErrParse, }, { name: "invalid signature", tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.InvalidSignatureToken, nil }, - wantErr: true, + wantErr: oidc.ErrSignatureUnsupportedAlg, }, { name: "wrong issuer", @@ -96,29 +106,7 @@ func TestVerifyIDTokenHint(t *testing.T) { tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "", ) }, - wantErr: true, - }, - { - name: "expired", - tokenClaims: func() (string, *oidc.IDTokenClaims) { - return tu.NewIDToken( - tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, - tu.ValidExpiration.Add(-time.Hour), tu.ValidAuthTime, tu.ValidNonce, - tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "", - ) - }, - wantErr: true, - }, - { - name: "wrong IAT", - tokenClaims: func() (string, *oidc.IDTokenClaims) { - return tu.NewIDToken( - tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, - tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce, - tu.ValidACR, tu.ValidAMR, tu.ValidClientID, -time.Hour, "", - ) - }, - wantErr: true, + wantErr: oidc.ErrIssuerInvalid, }, { name: "wrong acr", @@ -129,7 +117,31 @@ func TestVerifyIDTokenHint(t *testing.T) { "else", tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "", ) }, - wantErr: true, + wantErr: oidc.ErrAcrInvalid, + }, + { + name: "expired", + tokenClaims: func() (string, *oidc.IDTokenClaims) { + return tu.NewIDToken( + tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration.Add(-time.Hour), tu.ValidAuthTime, tu.ValidNonce, + tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "", + ) + }, + wantClaims: true, + wantErr: IDTokenHintExpiredError{oidc.ErrExpired}, + }, + { + name: "IAT too old", + tokenClaims: func() (string, *oidc.IDTokenClaims) { + return tu.NewIDToken( + tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce, + tu.ValidACR, tu.ValidAMR, tu.ValidClientID, time.Hour, "", + ) + }, + wantClaims: true, + wantErr: IDTokenHintExpiredError{oidc.ErrIatToOld}, }, { name: "expired auth", @@ -140,7 +152,8 @@ func TestVerifyIDTokenHint(t *testing.T) { tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "", ) }, - wantErr: true, + wantClaims: true, + wantErr: IDTokenHintExpiredError{oidc.ErrAuthTimeToOld}, }, } for _, tt := range tests { @@ -148,14 +161,12 @@ func TestVerifyIDTokenHint(t *testing.T) { token, want := tt.tokenClaims() got, err := VerifyIDTokenHint[*oidc.IDTokenClaims](context.Background(), token, verifier) - if tt.wantErr { - assert.Error(t, err) - assert.Nil(t, got) + require.ErrorIs(t, err, tt.wantErr) + if tt.wantClaims { + assert.Equal(t, got, want, "claims") return } - require.NoError(t, err) - require.NotNil(t, got) - assert.Equal(t, got, want) + assert.Nil(t, got, "claims") }) } } From 437a0497ab3454b8ce6cdd5310bae2d9c906d906 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 13:54:30 +0200 Subject: [PATCH 336/502] chore(deps): bump github.com/google/uuid from 1.5.0 to 1.6.0 (#523) Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.5.0 to 1.6.0. - [Release notes](https://github.com/google/uuid/releases) - [Changelog](https://github.com/google/uuid/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/uuid/compare/v1.5.0...v1.6.0) --- updated-dependencies: - dependency-name: github.com/google/uuid dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 45c80e5..eb53ce5 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/go-jose/go-jose/v3 v3.0.1 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 - github.com/google/uuid v1.5.0 + github.com/google/uuid v1.6.0 github.com/gorilla/securecookie v1.1.2 github.com/jeremija/gosubmit v0.2.7 github.com/muhlemmer/gu v0.3.1 diff --git a/go.sum b/go.sum index 8080fa9..f75fab7 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,8 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc= From e9bd7d7bac49692a814e4f89e01f572eb3a263a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 26 Jan 2024 17:44:50 +0200 Subject: [PATCH 337/502] feat(op): split the access and ID token hint verifiers (#525) * feat(op): split the access and ID token hint verifiers In zitadel we require different behaviors wrt public key expiry between access tokens and ID token hints. This change splits the two verifiers in the OP. The default is still based on Storage and passed to both verifier fields. * add new options to tests --- pkg/op/op.go | 59 ++++++++++++++++++++++++++--------------------- pkg/op/op_test.go | 8 +++++-- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/pkg/op/op.go b/pkg/op/op.go index fdc073c..14c5356 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -257,13 +257,16 @@ func NewForwardedOpenIDProvider(path string, config *Config, storage Storage, op // op.AuthCallbackURL(provider) which is probably /callback. On the redirect back // to the AuthCallbackURL, the request id should be passed as the "id" parameter. func NewProvider(config *Config, storage Storage, issuer func(insecure bool) (IssuerFromRequest, error), opOpts ...Option) (_ *Provider, err error) { + keySet := &OpenIDKeySet{storage} o := &Provider{ - config: config, - storage: storage, - endpoints: DefaultEndpoints, - timer: make(<-chan time.Time), - corsOpts: &defaultCORSOptions, - logger: slog.Default(), + config: config, + storage: storage, + accessTokenKeySet: keySet, + idTokenHinKeySet: keySet, + endpoints: DefaultEndpoints, + timer: make(<-chan time.Time), + corsOpts: &defaultCORSOptions, + logger: slog.Default(), } for _, optFunc := range opOpts { @@ -276,19 +279,11 @@ func NewProvider(config *Config, storage Storage, issuer func(insecure bool) (Is if err != nil { return nil, err } - o.Handler = CreateRouter(o, o.interceptors...) - o.decoder = schema.NewDecoder() o.decoder.IgnoreUnknownKeys(true) - o.encoder = oidc.NewEncoder() - o.crypto = NewAESCrypto(config.CryptoKey) - - // Avoid potential race conditions by calling these early - _ = o.openIDKeySet() // sets keySet - return o, nil } @@ -299,7 +294,8 @@ type Provider struct { insecure bool endpoints *Endpoints storage Storage - keySet *openIDKeySet + accessTokenKeySet oidc.KeySet + idTokenHinKeySet oidc.KeySet crypto Crypto decoder *schema.Decoder encoder *schema.Encoder @@ -435,7 +431,7 @@ func (o *Provider) Encoder() httphelper.Encoder { } func (o *Provider) IDTokenHintVerifier(ctx context.Context) *IDTokenHintVerifier { - return NewIDTokenHintVerifier(IssuerFromContext(ctx), o.openIDKeySet(), o.idTokenHintVerifierOpts...) + return NewIDTokenHintVerifier(IssuerFromContext(ctx), o.idTokenHinKeySet, o.idTokenHintVerifierOpts...) } func (o *Provider) JWTProfileVerifier(ctx context.Context) *JWTProfileVerifier { @@ -443,14 +439,7 @@ func (o *Provider) JWTProfileVerifier(ctx context.Context) *JWTProfileVerifier { } func (o *Provider) AccessTokenVerifier(ctx context.Context) *AccessTokenVerifier { - return NewAccessTokenVerifier(IssuerFromContext(ctx), o.openIDKeySet(), o.accessTokenVerifierOpts...) -} - -func (o *Provider) openIDKeySet() oidc.KeySet { - if o.keySet == nil { - o.keySet = &openIDKeySet{o.Storage()} - } - return o.keySet + return NewAccessTokenVerifier(IssuerFromContext(ctx), o.accessTokenKeySet, o.accessTokenVerifierOpts...) } func (o *Provider) Crypto() Crypto { @@ -480,13 +469,13 @@ func (o *Provider) HttpHandler() http.Handler { return o } -type openIDKeySet struct { +type OpenIDKeySet struct { Storage } // VerifySignature implements the oidc.KeySet interface // providing an implementation for the keys stored in the OP Storage interface -func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { +func (o *OpenIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { keySet, err := o.Storage.KeySet(ctx) if err != nil { return nil, fmt.Errorf("error fetching keys: %w", err) @@ -617,6 +606,15 @@ func WithHttpInterceptors(interceptors ...HttpInterceptor) Option { } } +// WithAccessTokenKeySet allows passing a KeySet with public keys for Access Token verification. +// The default KeySet uses the [Storage] interface +func WithAccessTokenKeySet(keySet oidc.KeySet) Option { + return func(o *Provider) error { + o.accessTokenKeySet = keySet + return nil + } +} + func WithAccessTokenVerifierOpts(opts ...AccessTokenVerifierOpt) Option { return func(o *Provider) error { o.accessTokenVerifierOpts = opts @@ -624,6 +622,15 @@ func WithAccessTokenVerifierOpts(opts ...AccessTokenVerifierOpt) Option { } } +// WithIDTokenHintKeySet allows passing a KeySet with public keys for ID Token Hint verification. +// The default KeySet uses the [Storage] interface. +func WithIDTokenHintKeySet(keySet oidc.KeySet) Option { + return func(o *Provider) error { + o.idTokenHinKeySet = keySet + return nil + } +} + func WithIDTokenHintVerifierOpts(opts ...IDTokenHintVerifierOpt) Option { return func(o *Provider) error { o.idTokenHintVerifierOpts = opts diff --git a/pkg/op/op_test.go b/pkg/op/op_test.go index f97f666..b2a758c 100644 --- a/pkg/op/op_test.go +++ b/pkg/op/op_test.go @@ -58,8 +58,12 @@ func init() { } func newTestProvider(config *op.Config) op.OpenIDProvider { - provider, err := op.NewOpenIDProvider(testIssuer, config, - storage.NewStorage(storage.NewUserStore(testIssuer)), op.WithAllowInsecure(), + storage := storage.NewStorage(storage.NewUserStore(testIssuer)) + keySet := &op.OpenIDKeySet{storage} + provider, err := op.NewOpenIDProvider(testIssuer, config, storage, + op.WithAllowInsecure(), + op.WithAccessTokenKeySet(keySet), + op.WithIDTokenHintKeySet(keySet), ) if err != nil { panic(err) From 35d9540fd74496d8dc059fbf2bd701f74872d884 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 11:56:16 +0200 Subject: [PATCH 338/502] chore(deps): bump codecov/codecov-action from 3.1.4 to 3.1.5 (#526) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3.1.4 to 3.1.5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v3.1.4...v3.1.5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 42f8d4a..93ffe69 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v3.1.4 + - uses: codecov/codecov-action@v3.1.5 with: file: ./profile.cov name: codecov-go From 045b59e5a55be97ba180fdaae96fb66302a03353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Thu, 1 Feb 2024 14:49:22 +0200 Subject: [PATCH 339/502] fix(op): allow expired id token hints in authorize (#527) Like https://github.com/zitadel/oidc/pull/522 for end session, this change allows passing an expired ID token hint to the authorize endpoint. --- pkg/op/auth_request.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index ed368eb..7058ebc 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -391,9 +391,9 @@ func ValidateAuthReqIDTokenHint(ctx context.Context, idTokenHint string, verifie return "", nil } claims, err := VerifyIDTokenHint[*oidc.TokenClaims](ctx, idTokenHint, verifier) - if err != nil { + if err != nil && !errors.As(err, &IDTokenHintExpiredError{}) { return "", oidc.ErrLoginRequired().WithDescription("The id_token_hint is invalid. " + - "If you have any questions, you may contact the administrator of the application.") + "If you have any questions, you may contact the administrator of the application.").WithParent(err) } return claims.GetSubject(), nil } From 2aa8a327f667db92b616460a378d93a906f10274 Mon Sep 17 00:00:00 2001 From: Fabi Date: Fri, 2 Feb 2024 11:57:41 +0100 Subject: [PATCH 340/502] chore: update pm board action (#528) * chore: update pm board action automatically ad prs of non engineers to board and label community prs * Update issue.yml --- .github/workflows/issue.yml | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 362443d..69cb2ae 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -4,15 +4,40 @@ on: issues: types: - opened + pull_request_target: + types: + - opened jobs: add-to-project: - name: Add issue to project + name: Add issue and community pr to project runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@v0.5.0 + - name: add issue + uses: actions/add-to-project@v0.5.0 + if: ${{ github.event_name == 'issues' }} with: # You can target a repository in a different organization # to the issue project-url: https://github.com/orgs/zitadel/projects/2 github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} + - uses: tspascoal/get-user-teams-membership@v3 + id: checkUserMember + with: + username: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_PAT }} + - name: add pr + uses: actions/add-to-project@v0.5.0 + if: ${{ github.event_name == 'pull_request_target' && !contains(steps.checkUserMember.outputs.teams, 'engineers')}} + with: + # You can target a repository in a different organization + # to the issue + project-url: https://github.com/orgs/zitadel/projects/2 + github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} + - uses: actions-ecosystem/action-add-labels@v1.1.0 + if: ${{ github.event_name == 'pull_request_target' && !contains(steps.checkUserMember.outputs.teams, 'staff')}} + with: + github_token: ${{ secrets.ADD_TO_PROJECT_PAT }} + labels: | + os-contribution + auth From 984346f9ef6563b65cf10dfa658b22cca5dc0839 Mon Sep 17 00:00:00 2001 From: Fabi Date: Fri, 2 Feb 2024 14:33:52 +0100 Subject: [PATCH 341/502] chore: remove dependabot prs (#529) --- .github/workflows/issue.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 69cb2ae..4184654 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -28,16 +28,15 @@ jobs: GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_PAT }} - name: add pr uses: actions/add-to-project@v0.5.0 - if: ${{ github.event_name == 'pull_request_target' && !contains(steps.checkUserMember.outputs.teams, 'engineers')}} + if: ${{ github.event_name == 'pull_request_target' && !contains(steps.checkUserMember.outputs.teams, 'engineers') && github.actor != 'app/dependabot'}} with: # You can target a repository in a different organization # to the issue project-url: https://github.com/orgs/zitadel/projects/2 github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} - uses: actions-ecosystem/action-add-labels@v1.1.0 - if: ${{ github.event_name == 'pull_request_target' && !contains(steps.checkUserMember.outputs.teams, 'staff')}} + if: ${{ github.event_name == 'pull_request_target' && !contains(steps.checkUserMember.outputs.teams, 'staff') && github.actor != 'app/dependabot'}} with: github_token: ${{ secrets.ADD_TO_PROJECT_PAT }} labels: | os-contribution - auth From 25e103b24306fe80560b6281bfe0aecf2d513fb1 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Wed, 7 Feb 2024 07:30:04 +0100 Subject: [PATCH 342/502] chore: ignore dependabot for board PRs --- .github/workflows/issue.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 4184654..83d8ea3 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -23,19 +23,20 @@ jobs: github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} - uses: tspascoal/get-user-teams-membership@v3 id: checkUserMember + if: github.actor == 'dependabot[bot]' with: username: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_PAT }} - name: add pr uses: actions/add-to-project@v0.5.0 - if: ${{ github.event_name == 'pull_request_target' && !contains(steps.checkUserMember.outputs.teams, 'engineers') && github.actor != 'app/dependabot'}} + if: ${{ github.event_name == 'pull_request_target' && github.actor == 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'engineers')}} with: # You can target a repository in a different organization # to the issue project-url: https://github.com/orgs/zitadel/projects/2 github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} - uses: actions-ecosystem/action-add-labels@v1.1.0 - if: ${{ github.event_name == 'pull_request_target' && !contains(steps.checkUserMember.outputs.teams, 'staff') && github.actor != 'app/dependabot'}} + if: ${{ github.event_name == 'pull_request_target' && github.actor == 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'staff')}} with: github_token: ${{ secrets.ADD_TO_PROJECT_PAT }} labels: | From 7a45a8645226a786191b4ee73e47713bdb09a441 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Feb 2024 18:26:46 +0200 Subject: [PATCH 343/502] chore(deps): bump codecov/codecov-action from 3.1.5 to 4.0.1 (#531) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3.1.5 to 4.0.1. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v3.1.5...v4.0.1) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 93ffe69..6f92575 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v3.1.5 + - uses: codecov/codecov-action@v4.0.1 with: file: ./profile.cov name: codecov-go From 34f44325b8191c0997a9995521c653ccee4a30f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Feb 2024 18:29:38 +0200 Subject: [PATCH 344/502] chore(deps): bump go.opentelemetry.io/otel/trace from 1.22.0 to 1.23.0 (#534) Bumps [go.opentelemetry.io/otel/trace](https://github.com/open-telemetry/opentelemetry-go) from 1.22.0 to 1.23.0. - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.22.0...v1.23.0) --- updated-dependencies: - dependency-name: go.opentelemetry.io/otel/trace dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index eb53ce5..32a8951 100644 --- a/go.mod +++ b/go.mod @@ -18,8 +18,8 @@ require ( github.com/stretchr/testify v1.8.4 github.com/zitadel/logging v0.5.0 github.com/zitadel/schema v1.3.0 - go.opentelemetry.io/otel v1.22.0 - go.opentelemetry.io/otel/trace v1.22.0 + go.opentelemetry.io/otel v1.23.0 + go.opentelemetry.io/otel/trace v1.23.0 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 golang.org/x/oauth2 v0.16.0 golang.org/x/text v0.14.0 @@ -32,7 +32,7 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel/metric v1.22.0 // indirect + go.opentelemetry.io/otel/metric v1.23.0 // indirect golang.org/x/crypto v0.18.0 // indirect golang.org/x/net v0.20.0 // indirect golang.org/x/sys v0.16.0 // indirect diff --git a/go.sum b/go.sum index f75fab7..f3d8daf 100644 --- a/go.sum +++ b/go.sum @@ -57,12 +57,12 @@ github.com/zitadel/logging v0.5.0 h1:Kunouvqse/efXy4UDvFw5s3vP+Z4AlHo3y8wF7stXHA github.com/zitadel/logging v0.5.0/go.mod h1:IzP5fzwFhzzyxHkSmfF8dsyqFsQRJLLcQmwhIBzlGsE= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= -go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= -go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= -go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= -go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= -go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= -go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= +go.opentelemetry.io/otel v1.23.0 h1:Df0pqjqExIywbMCMTxkAwzjLZtRf+bBKLbUcpxO2C9E= +go.opentelemetry.io/otel v1.23.0/go.mod h1:YCycw9ZeKhcJFrb34iVSkyT0iczq/zYDtZYFufObyB0= +go.opentelemetry.io/otel/metric v1.23.0 h1:pazkx7ss4LFVVYSxYew7L5I6qvLXHA0Ap2pwV+9Cnpo= +go.opentelemetry.io/otel/metric v1.23.0/go.mod h1:MqUW2X2a6Q8RN96E2/nqNoT+z9BSms20Jb7Bbp+HiTo= +go.opentelemetry.io/otel/trace v1.23.0 h1:37Ik5Ib7xfYVb4V1UtnT97T1jI+AoIYkJyPkuL4iJgI= +go.opentelemetry.io/otel/trace v1.23.0/go.mod h1:GSGTbIClEsuZrGIzoEHqsVfxgn5UkggkflQwDScNUsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= From 3ea61738604d6e0f4a1565401500054f3fd64bca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Feb 2024 18:30:44 +0200 Subject: [PATCH 345/502] chore(deps): bump actions-ecosystem/action-add-labels (#530) Bumps [actions-ecosystem/action-add-labels](https://github.com/actions-ecosystem/action-add-labels) from 1.1.0 to 1.1.3. - [Release notes](https://github.com/actions-ecosystem/action-add-labels/releases) - [Commits](https://github.com/actions-ecosystem/action-add-labels/compare/v1.1.0...v1.1.3) --- updated-dependencies: - dependency-name: actions-ecosystem/action-add-labels dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/issue.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 83d8ea3..1409ff1 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -35,7 +35,7 @@ jobs: # to the issue project-url: https://github.com/orgs/zitadel/projects/2 github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} - - uses: actions-ecosystem/action-add-labels@v1.1.0 + - uses: actions-ecosystem/action-add-labels@v1.1.3 if: ${{ github.event_name == 'pull_request_target' && github.actor == 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'staff')}} with: github_token: ${{ secrets.ADD_TO_PROJECT_PAT }} From ee8152f19eecd5d490299c0ddd993e4324cb2432 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Fri, 9 Feb 2024 16:11:59 +0100 Subject: [PATCH 346/502] chore: ignore dependabot for board PRs --- .github/workflows/issue.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 1409ff1..62fd01d 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -23,20 +23,20 @@ jobs: github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} - uses: tspascoal/get-user-teams-membership@v3 id: checkUserMember - if: github.actor == 'dependabot[bot]' + if: github.actor != 'dependabot[bot]' with: username: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_PAT }} - name: add pr uses: actions/add-to-project@v0.5.0 - if: ${{ github.event_name == 'pull_request_target' && github.actor == 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'engineers')}} + if: ${{ github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'engineers')}} with: # You can target a repository in a different organization # to the issue project-url: https://github.com/orgs/zitadel/projects/2 github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} - uses: actions-ecosystem/action-add-labels@v1.1.3 - if: ${{ github.event_name == 'pull_request_target' && github.actor == 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'staff')}} + if: ${{ github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'staff')}} with: github_token: ${{ secrets.ADD_TO_PROJECT_PAT }} labels: | From 625a4e480d2d06819c345ed94c7b506727b0532a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Feb 2024 16:14:24 +0100 Subject: [PATCH 347/502] chore(deps): bump go.opentelemetry.io/otel/trace from 1.23.0 to 1.23.1 (#539) Bumps [go.opentelemetry.io/otel/trace](https://github.com/open-telemetry/opentelemetry-go) from 1.23.0 to 1.23.1. - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.23.0...v1.23.1) --- updated-dependencies: - dependency-name: go.opentelemetry.io/otel/trace dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 32a8951..30e5abe 100644 --- a/go.mod +++ b/go.mod @@ -18,8 +18,8 @@ require ( github.com/stretchr/testify v1.8.4 github.com/zitadel/logging v0.5.0 github.com/zitadel/schema v1.3.0 - go.opentelemetry.io/otel v1.23.0 - go.opentelemetry.io/otel/trace v1.23.0 + go.opentelemetry.io/otel v1.23.1 + go.opentelemetry.io/otel/trace v1.23.1 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 golang.org/x/oauth2 v0.16.0 golang.org/x/text v0.14.0 @@ -32,7 +32,7 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel/metric v1.23.0 // indirect + go.opentelemetry.io/otel/metric v1.23.1 // indirect golang.org/x/crypto v0.18.0 // indirect golang.org/x/net v0.20.0 // indirect golang.org/x/sys v0.16.0 // indirect diff --git a/go.sum b/go.sum index f3d8daf..5639d44 100644 --- a/go.sum +++ b/go.sum @@ -57,12 +57,12 @@ github.com/zitadel/logging v0.5.0 h1:Kunouvqse/efXy4UDvFw5s3vP+Z4AlHo3y8wF7stXHA github.com/zitadel/logging v0.5.0/go.mod h1:IzP5fzwFhzzyxHkSmfF8dsyqFsQRJLLcQmwhIBzlGsE= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= -go.opentelemetry.io/otel v1.23.0 h1:Df0pqjqExIywbMCMTxkAwzjLZtRf+bBKLbUcpxO2C9E= -go.opentelemetry.io/otel v1.23.0/go.mod h1:YCycw9ZeKhcJFrb34iVSkyT0iczq/zYDtZYFufObyB0= -go.opentelemetry.io/otel/metric v1.23.0 h1:pazkx7ss4LFVVYSxYew7L5I6qvLXHA0Ap2pwV+9Cnpo= -go.opentelemetry.io/otel/metric v1.23.0/go.mod h1:MqUW2X2a6Q8RN96E2/nqNoT+z9BSms20Jb7Bbp+HiTo= -go.opentelemetry.io/otel/trace v1.23.0 h1:37Ik5Ib7xfYVb4V1UtnT97T1jI+AoIYkJyPkuL4iJgI= -go.opentelemetry.io/otel/trace v1.23.0/go.mod h1:GSGTbIClEsuZrGIzoEHqsVfxgn5UkggkflQwDScNUsk= +go.opentelemetry.io/otel v1.23.1 h1:Za4UzOqJYS+MUczKI320AtqZHZb7EqxO00jAHE0jmQY= +go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA= +go.opentelemetry.io/otel/metric v1.23.1 h1:PQJmqJ9u2QaJLBOELl1cxIdPcpbwzbkjfEyelTl2rlo= +go.opentelemetry.io/otel/metric v1.23.1/go.mod h1:mpG2QPlAfnK8yNhNJAxDZruU9Y1/HubbC+KyH8FaCWI= +go.opentelemetry.io/otel/trace v1.23.1 h1:4LrmmEd8AU2rFvU1zegmvqW7+kWarxtNOPyeL6HmYY8= +go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1KcVmuW7dwFSVrI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= From 1eebaf8d6f0c47241ddc1ea69e17729bdf947f1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Feb 2024 16:14:46 +0100 Subject: [PATCH 348/502] chore(deps): bump go.opentelemetry.io/otel from 1.23.0 to 1.23.1 (#540) Bumps [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) from 1.23.0 to 1.23.1. - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.23.0...v1.23.1) --- updated-dependencies: - dependency-name: go.opentelemetry.io/otel dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From da8b73f3425ae9e0e3836db5cadc101541952e5e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Feb 2024 15:16:10 +0000 Subject: [PATCH 349/502] chore(deps): bump golang.org/x/oauth2 from 0.16.0 to 0.17.0 (#542) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.16.0 to 0.17.0. - [Commits](https://github.com/golang/oauth2/compare/v0.16.0...v0.17.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 30e5abe..534c5e6 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( go.opentelemetry.io/otel v1.23.1 go.opentelemetry.io/otel/trace v1.23.1 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 - golang.org/x/oauth2 v0.16.0 + golang.org/x/oauth2 v0.17.0 golang.org/x/text v0.14.0 ) @@ -33,9 +33,9 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/otel/metric v1.23.1 // indirect - golang.org/x/crypto v0.18.0 // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/sys v0.16.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 5639d44..aee4334 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1Kc golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -77,11 +77,11 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= -golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= +golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -90,8 +90,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= From 3e593474e989be842afcaefb4111418cee6d3939 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 12:14:41 +0200 Subject: [PATCH 350/502] chore(deps): bump github.com/go-chi/chi/v5 from 5.0.11 to 5.0.12 (#548) Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.0.11 to 5.0.12. - [Release notes](https://github.com/go-chi/chi/releases) - [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md) - [Commits](https://github.com/go-chi/chi/compare/v5.0.11...v5.0.12) --- updated-dependencies: - dependency-name: github.com/go-chi/chi/v5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 534c5e6..8b3865b 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.19 require ( github.com/bmatcuk/doublestar/v4 v4.6.1 - github.com/go-chi/chi/v5 v5.0.11 + github.com/go-chi/chi/v5 v5.0.12 github.com/go-jose/go-jose/v3 v3.0.1 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 diff --git a/go.sum b/go.sum index aee4334..3ffd38c 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTS 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/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= -github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= +github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= From b45072a4c0c978366c63ca53c048bb9e1a35e375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Wed, 21 Feb 2024 11:17:00 +0100 Subject: [PATCH 351/502] fix: Set unauthorizedHandler, if not defined (#547) --- pkg/client/rp/relying_party.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 04be4ef..6105b2f 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -167,6 +167,9 @@ func (rp *relyingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, } func (rp *relyingParty) UnauthorizedHandler() func(http.ResponseWriter, *http.Request, string, string) { + if rp.unauthorizedHandler == nil { + rp.unauthorizedHandler = DefaultUnauthorizedHandler + } return rp.unauthorizedHandler } @@ -196,8 +199,9 @@ func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingPart } // avoid races by calling these early - _ = rp.IDTokenVerifier() // sets idTokenVerifier - _ = rp.ErrorHandler() // sets errorHandler + _ = rp.IDTokenVerifier() // sets idTokenVerifier + _ = rp.ErrorHandler() // sets errorHandler + _ = rp.UnauthorizedHandler() // sets unauthorizedHandler return rp, nil } @@ -233,8 +237,9 @@ func NewRelyingPartyOIDC(ctx context.Context, issuer, clientID, clientSecret, re rp.endpoints = endpoints // avoid races by calling these early - _ = rp.IDTokenVerifier() // sets idTokenVerifier - _ = rp.ErrorHandler() // sets errorHandler + _ = rp.IDTokenVerifier() // sets idTokenVerifier + _ = rp.ErrorHandler() // sets errorHandler + _ = rp.UnauthorizedHandler() // sets unauthorizedHandler return rp, nil } From f4bbffb51b60be934f1b79082de71d8aba4f6eaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Fri, 23 Feb 2024 11:18:06 +0100 Subject: [PATCH 352/502] feat: Add rp.WithAuthStyle as Option (#546) * feat: Add rp.WithAuthStyle as Option * Update integration_test.go * Update integration_test.go * Update integration_test.go --- pkg/client/integration_test.go | 2 ++ pkg/client/rp/relying_party.go | 25 ++++++++++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/pkg/client/integration_test.go b/pkg/client/integration_test.go index 7d4cd9e..ce77f5e 100644 --- a/pkg/client/integration_test.go +++ b/pkg/client/integration_test.go @@ -21,6 +21,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/exp/slog" + "golang.org/x/oauth2" "github.com/zitadel/oidc/v3/example/server/exampleop" "github.com/zitadel/oidc/v3/example/server/storage" @@ -217,6 +218,7 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID, targetURL, []string{"openid", "email", "profile", "offline_access"}, rp.WithPKCE(cookieHandler), + rp.WithAuthStyle(oauth2.AuthStyleInHeader), rp.WithVerifierOpts( rp.WithIssuedAtOffset(5*time.Second), rp.WithSupportedSigningAlgorithms("RS256", "RS384", "RS512", "ES256", "ES384", "ES512"), diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 6105b2f..d4bc13c 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -100,6 +100,8 @@ type relyingParty struct { httpClient *http.Client cookieHandler *httphelper.CookieHandler + oauthAuthStyle oauth2.AuthStyle + errorHandler func(http.ResponseWriter, *http.Request, string, string, string) unauthorizedHandler func(http.ResponseWriter, *http.Request, string, string) idTokenVerifier *IDTokenVerifier @@ -190,6 +192,7 @@ func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingPart httpClient: httphelper.DefaultHTTPClient, oauth2Only: true, unauthorizedHandler: DefaultUnauthorizedHandler, + oauthAuthStyle: oauth2.AuthStyleAutoDetect, } for _, optFunc := range options { @@ -198,6 +201,8 @@ func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingPart } } + rp.oauthConfig.Endpoint.AuthStyle = rp.oauthAuthStyle + // avoid races by calling these early _ = rp.IDTokenVerifier() // sets idTokenVerifier _ = rp.ErrorHandler() // sets errorHandler @@ -218,8 +223,9 @@ func NewRelyingPartyOIDC(ctx context.Context, issuer, clientID, clientSecret, re RedirectURL: redirectURI, Scopes: scopes, }, - httpClient: httphelper.DefaultHTTPClient, - oauth2Only: false, + httpClient: httphelper.DefaultHTTPClient, + oauth2Only: false, + oauthAuthStyle: oauth2.AuthStyleAutoDetect, } for _, optFunc := range options { @@ -236,6 +242,9 @@ func NewRelyingPartyOIDC(ctx context.Context, issuer, clientID, clientSecret, re rp.oauthConfig.Endpoint = endpoints.Endpoint rp.endpoints = endpoints + rp.oauthConfig.Endpoint.AuthStyle = rp.oauthAuthStyle + rp.endpoints.Endpoint.AuthStyle = rp.oauthAuthStyle + // avoid races by calling these early _ = rp.IDTokenVerifier() // sets idTokenVerifier _ = rp.ErrorHandler() // sets errorHandler @@ -295,6 +304,13 @@ func WithUnauthorizedHandler(unauthorizedHandler UnauthorizedHandler) Option { } } +func WithAuthStyle(oauthAuthStyle oauth2.AuthStyle) Option { + return func(rp *relyingParty) error { + rp.oauthAuthStyle = oauthAuthStyle + return nil + } +} + func WithVerifierOpts(opts ...VerifierOption) Option { return func(rp *relyingParty) error { rp.verifierOpts = opts @@ -594,9 +610,8 @@ type Endpoints struct { func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints { return Endpoints{ Endpoint: oauth2.Endpoint{ - AuthURL: discoveryConfig.AuthorizationEndpoint, - AuthStyle: oauth2.AuthStyleAutoDetect, - TokenURL: discoveryConfig.TokenEndpoint, + AuthURL: discoveryConfig.AuthorizationEndpoint, + TokenURL: discoveryConfig.TokenEndpoint, }, IntrospectURL: discoveryConfig.IntrospectionEndpoint, UserinfoURL: discoveryConfig.UserinfoEndpoint, From a6a206b021a4c00a73f7a0a92a6abd8ebba78fa1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 10:45:58 +0200 Subject: [PATCH 353/502] chore(deps): bump go.opentelemetry.io/otel/trace from 1.23.1 to 1.24.0 (#556) Bumps [go.opentelemetry.io/otel/trace](https://github.com/open-telemetry/opentelemetry-go) from 1.23.1 to 1.24.0. - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.23.1...v1.24.0) --- updated-dependencies: - dependency-name: go.opentelemetry.io/otel/trace dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 8b3865b..10ed4c1 100644 --- a/go.mod +++ b/go.mod @@ -18,8 +18,8 @@ require ( github.com/stretchr/testify v1.8.4 github.com/zitadel/logging v0.5.0 github.com/zitadel/schema v1.3.0 - go.opentelemetry.io/otel v1.23.1 - go.opentelemetry.io/otel/trace v1.23.1 + go.opentelemetry.io/otel v1.24.0 + go.opentelemetry.io/otel/trace v1.24.0 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 golang.org/x/oauth2 v0.17.0 golang.org/x/text v0.14.0 @@ -32,7 +32,7 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel/metric v1.23.1 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect golang.org/x/crypto v0.19.0 // indirect golang.org/x/net v0.21.0 // indirect golang.org/x/sys v0.17.0 // indirect diff --git a/go.sum b/go.sum index 3ffd38c..d38a7f2 100644 --- a/go.sum +++ b/go.sum @@ -57,12 +57,12 @@ github.com/zitadel/logging v0.5.0 h1:Kunouvqse/efXy4UDvFw5s3vP+Z4AlHo3y8wF7stXHA github.com/zitadel/logging v0.5.0/go.mod h1:IzP5fzwFhzzyxHkSmfF8dsyqFsQRJLLcQmwhIBzlGsE= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= -go.opentelemetry.io/otel v1.23.1 h1:Za4UzOqJYS+MUczKI320AtqZHZb7EqxO00jAHE0jmQY= -go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA= -go.opentelemetry.io/otel/metric v1.23.1 h1:PQJmqJ9u2QaJLBOELl1cxIdPcpbwzbkjfEyelTl2rlo= -go.opentelemetry.io/otel/metric v1.23.1/go.mod h1:mpG2QPlAfnK8yNhNJAxDZruU9Y1/HubbC+KyH8FaCWI= -go.opentelemetry.io/otel/trace v1.23.1 h1:4LrmmEd8AU2rFvU1zegmvqW7+kWarxtNOPyeL6HmYY8= -go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1KcVmuW7dwFSVrI= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= From b93f625088b6311a2e7f4db26d3094f0a0bf5c7d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 10:47:11 +0200 Subject: [PATCH 354/502] chore(deps): bump github.com/go-jose/go-jose/v3 from 3.0.1 to 3.0.2 (#554) Bumps [github.com/go-jose/go-jose/v3](https://github.com/go-jose/go-jose) from 3.0.1 to 3.0.2. - [Release notes](https://github.com/go-jose/go-jose/releases) - [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md) - [Commits](https://github.com/go-jose/go-jose/compare/v3.0.1...v3.0.2) --- updated-dependencies: - dependency-name: github.com/go-jose/go-jose/v3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 32 +++++++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 10ed4c1..d1c5f2b 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.19 require ( github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/go-chi/chi/v5 v5.0.12 - github.com/go-jose/go-jose/v3 v3.0.1 + github.com/go-jose/go-jose/v3 v3.0.2 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index d38a7f2..f84f80e 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ 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/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= -github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-jose/go-jose/v3 v3.0.2 h1:2Edjn8Nrb44UvTdp84KU0bBPs1cO7noRCybtS3eJEUQ= +github.com/go-jose/go-jose/v3 v3.0.2/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -19,9 +19,9 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo= github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= @@ -48,11 +48,11 @@ github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zitadel/logging v0.5.0 h1:Kunouvqse/efXy4UDvFw5s3vP+Z4AlHo3y8wF7stXHA= github.com/zitadel/logging v0.5.0/go.mod h1:IzP5fzwFhzzyxHkSmfF8dsyqFsQRJLLcQmwhIBzlGsE= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= @@ -64,19 +64,25 @@ go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8p go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -84,23 +90,39 @@ golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 385060930dd4438ac4589f211034caecf48fa497 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 10:05:38 +0200 Subject: [PATCH 355/502] chore(deps): bump actions/add-to-project from 0.5.0 to 0.6.0 (#558) Bumps [actions/add-to-project](https://github.com/actions/add-to-project) from 0.5.0 to 0.6.0. - [Release notes](https://github.com/actions/add-to-project/releases) - [Commits](https://github.com/actions/add-to-project/compare/v0.5.0...v0.6.0) --- updated-dependencies: - dependency-name: actions/add-to-project dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/issue.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 62fd01d..a2b56eb 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: add issue - uses: actions/add-to-project@v0.5.0 + uses: actions/add-to-project@v0.6.0 if: ${{ github.event_name == 'issues' }} with: # You can target a repository in a different organization @@ -28,7 +28,7 @@ jobs: username: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_PAT }} - name: add pr - uses: actions/add-to-project@v0.5.0 + uses: actions/add-to-project@v0.6.0 if: ${{ github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'engineers')}} with: # You can target a repository in a different organization From 38c025f7f8948484cd044dcf8d466fb210a2e022 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 10:09:14 +0200 Subject: [PATCH 356/502] chore(deps): bump codecov/codecov-action from 4.0.1 to 4.1.0 (#559) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.0.1 to 4.1.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4.0.1...v4.1.0) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6f92575..12b9007 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v4.0.1 + - uses: codecov/codecov-action@v4.1.0 with: file: ./profile.cov name: codecov-go From 972b8981e5e10fde590d9dd426702ab525f33de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 28 Feb 2024 11:44:14 +0200 Subject: [PATCH 357/502] feat: go 1.22 and slog migration (#557) This change adds Go 1.22 as a build target and drops support for Go 1.20 and older. The golang.org/x/exp/slog import is migrated to log/slog. Slog has been part of the Go standard library since Go 1.21. Therefore we are dropping support for older Go versions. This is in line of our support policy of "the latest two Go versions". --- .github/workflows/release.yml | 2 +- README.md | 5 +- example/client/app/app.go | 2 +- example/server/exampleop/op.go | 2 +- example/server/main.go | 2 +- example/server/storage/oidc.go | 2 +- go.mod | 5 +- go.sum | 12 +++-- pkg/client/integration_test.go | 2 +- pkg/client/rp/log.go | 2 +- pkg/client/rp/relying_party.go | 2 +- pkg/oidc/authorization.go | 2 +- pkg/oidc/authorization_test.go | 2 +- pkg/oidc/error.go | 3 +- pkg/oidc/error_go120_test.go | 83 ---------------------------------- pkg/oidc/error_test.go | 74 +++++++++++++++++++++++++++++- pkg/op/auth_request.go | 2 +- pkg/op/auth_request_test.go | 2 +- pkg/op/error.go | 2 +- pkg/op/error_test.go | 2 +- pkg/op/mock/authorizer.mock.go | 2 +- pkg/op/op.go | 6 +-- pkg/op/server_http.go | 2 +- pkg/op/server_http_test.go | 2 +- pkg/op/session.go | 2 +- pkg/op/token_request.go | 2 +- 26 files changed, 106 insertions(+), 120 deletions(-) delete mode 100644 pkg/oidc/error_go120_test.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 12b9007..a4a8f87 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - go: ['1.19', '1.20', '1.21'] + go: ['1.21', '1.22'] name: Go ${{ matrix.go }} test steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index 7f1a610..72ae8d4 100644 --- a/README.md +++ b/README.md @@ -115,10 +115,9 @@ Versions that also build are marked with :warning:. | Version | Supported | | ------- | ------------------ | -| <1.19 | :x: | -| 1.19 | :warning: | -| 1.20 | :white_check_mark: | +| <1.21 | :x: | | 1.21 | :white_check_mark: | +| 1.22 | :white_check_mark: | ## Why another library diff --git a/example/client/app/app.go b/example/client/app/app.go index 0e339f4..a779169 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "log/slog" "net/http" "os" "strings" @@ -12,7 +13,6 @@ import ( "github.com/google/uuid" "github.com/sirupsen/logrus" - "golang.org/x/exp/slog" "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/client/rp" diff --git a/example/server/exampleop/op.go b/example/server/exampleop/op.go index baa2662..893628e 100644 --- a/example/server/exampleop/op.go +++ b/example/server/exampleop/op.go @@ -3,13 +3,13 @@ package exampleop import ( "crypto/sha256" "log" + "log/slog" "net/http" "sync/atomic" "time" "github.com/go-chi/chi/v5" "github.com/zitadel/logging" - "golang.org/x/exp/slog" "golang.org/x/text/language" "github.com/zitadel/oidc/v3/example/server/storage" diff --git a/example/server/main.go b/example/server/main.go index 38057fb..a2ad190 100644 --- a/example/server/main.go +++ b/example/server/main.go @@ -2,12 +2,12 @@ package main import ( "fmt" + "log/slog" "net/http" "os" "github.com/zitadel/oidc/v3/example/server/exampleop" "github.com/zitadel/oidc/v3/example/server/storage" - "golang.org/x/exp/slog" ) func main() { diff --git a/example/server/storage/oidc.go b/example/server/storage/oidc.go index 63afcf9..2509f77 100644 --- a/example/server/storage/oidc.go +++ b/example/server/storage/oidc.go @@ -1,9 +1,9 @@ package storage import ( + "log/slog" "time" - "golang.org/x/exp/slog" "golang.org/x/text/language" "github.com/zitadel/oidc/v3/pkg/oidc" diff --git a/go.mod b/go.mod index d1c5f2b..2ae42cb 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/zitadel/oidc/v3 -go 1.19 +go 1.21 require ( github.com/bmatcuk/doublestar/v4 v4.6.1 @@ -16,11 +16,10 @@ require ( github.com/rs/cors v1.10.1 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 - github.com/zitadel/logging v0.5.0 + github.com/zitadel/logging v0.6.0 github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.24.0 go.opentelemetry.io/otel/trace v1.24.0 - golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 golang.org/x/oauth2 v0.17.0 golang.org/x/text v0.14.0 ) diff --git a/go.sum b/go.sum index f84f80e..42363b9 100644 --- a/go.sum +++ b/go.sum @@ -23,12 +23,14 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo= github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= @@ -36,7 +38,9 @@ github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pw github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc= github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= @@ -53,8 +57,8 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zitadel/logging v0.5.0 h1:Kunouvqse/efXy4UDvFw5s3vP+Z4AlHo3y8wF7stXHA= -github.com/zitadel/logging v0.5.0/go.mod h1:IzP5fzwFhzzyxHkSmfF8dsyqFsQRJLLcQmwhIBzlGsE= +github.com/zitadel/logging v0.6.0 h1:t5Nnt//r+m2ZhhoTmoPX+c96pbMarqJvW1Vq6xFTank= +github.com/zitadel/logging v0.6.0/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= @@ -68,8 +72,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= -golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -136,7 +138,9 @@ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/client/integration_test.go b/pkg/client/integration_test.go index ce77f5e..9145c1e 100644 --- a/pkg/client/integration_test.go +++ b/pkg/client/integration_test.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "io" + "log/slog" "math/rand" "net/http" "net/http/cookiejar" @@ -20,7 +21,6 @@ import ( "github.com/jeremija/gosubmit" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/exp/slog" "golang.org/x/oauth2" "github.com/zitadel/oidc/v3/example/server/exampleop" diff --git a/pkg/client/rp/log.go b/pkg/client/rp/log.go index 6056fa2..556220c 100644 --- a/pkg/client/rp/log.go +++ b/pkg/client/rp/log.go @@ -2,9 +2,9 @@ package rp import ( "context" + "log/slog" "github.com/zitadel/logging" - "golang.org/x/exp/slog" ) func logCtxWithRPData(ctx context.Context, rp RelyingParty, attrs ...any) context.Context { diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index d4bc13c..72270fe 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "errors" + "log/slog" "net/http" "net/url" "time" @@ -11,7 +12,6 @@ import ( "github.com/go-jose/go-jose/v3" "github.com/google/uuid" "github.com/zitadel/logging" - "golang.org/x/exp/slog" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" diff --git a/pkg/oidc/authorization.go b/pkg/oidc/authorization.go index 511e396..89139ba 100644 --- a/pkg/oidc/authorization.go +++ b/pkg/oidc/authorization.go @@ -1,7 +1,7 @@ package oidc import ( - "golang.org/x/exp/slog" + "log/slog" ) const ( diff --git a/pkg/oidc/authorization_test.go b/pkg/oidc/authorization_test.go index 573d65c..1446efa 100644 --- a/pkg/oidc/authorization_test.go +++ b/pkg/oidc/authorization_test.go @@ -3,10 +3,10 @@ package oidc import ( + "log/slog" "testing" "github.com/stretchr/testify/assert" - "golang.org/x/exp/slog" ) func TestAuthRequest_LogValue(t *testing.T) { diff --git a/pkg/oidc/error.go b/pkg/oidc/error.go index b690a23..86f8724 100644 --- a/pkg/oidc/error.go +++ b/pkg/oidc/error.go @@ -3,8 +3,7 @@ package oidc import ( "errors" "fmt" - - "golang.org/x/exp/slog" + "log/slog" ) type errorType string diff --git a/pkg/oidc/error_go120_test.go b/pkg/oidc/error_go120_test.go deleted file mode 100644 index 399d7f7..0000000 --- a/pkg/oidc/error_go120_test.go +++ /dev/null @@ -1,83 +0,0 @@ -//go:build go1.20 - -package oidc - -import ( - "io" - "testing" - - "github.com/stretchr/testify/assert" - "golang.org/x/exp/slog" -) - -func TestError_LogValue(t *testing.T) { - type fields struct { - Parent error - ErrorType errorType - Description string - State string - redirectDisabled bool - } - tests := []struct { - name string - fields fields - want slog.Value - }{ - { - name: "parent", - fields: fields{ - Parent: io.EOF, - }, - want: slog.GroupValue(slog.Any("parent", io.EOF)), - }, - { - name: "description", - fields: fields{ - Description: "oops", - }, - want: slog.GroupValue(slog.String("description", "oops")), - }, - { - name: "errorType", - fields: fields{ - ErrorType: ExpiredToken, - }, - want: slog.GroupValue(slog.String("type", string(ExpiredToken))), - }, - { - name: "state", - fields: fields{ - State: "123", - }, - want: slog.GroupValue(slog.String("state", "123")), - }, - { - name: "all fields", - fields: fields{ - Parent: io.EOF, - Description: "oops", - ErrorType: ExpiredToken, - State: "123", - }, - want: slog.GroupValue( - slog.Any("parent", io.EOF), - slog.String("description", "oops"), - slog.String("type", string(ExpiredToken)), - slog.String("state", "123"), - ), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - e := &Error{ - Parent: tt.fields.Parent, - ErrorType: tt.fields.ErrorType, - Description: tt.fields.Description, - State: tt.fields.State, - redirectDisabled: tt.fields.redirectDisabled, - } - got := e.LogValue() - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/pkg/oidc/error_test.go b/pkg/oidc/error_test.go index 0554c8f..2eeb4e6 100644 --- a/pkg/oidc/error_test.go +++ b/pkg/oidc/error_test.go @@ -2,10 +2,10 @@ package oidc import ( "io" + "log/slog" "testing" "github.com/stretchr/testify/assert" - "golang.org/x/exp/slog" ) func TestDefaultToServerError(t *testing.T) { @@ -79,3 +79,75 @@ func TestError_LogLevel(t *testing.T) { }) } } + +func TestError_LogValue(t *testing.T) { + type fields struct { + Parent error + ErrorType errorType + Description string + State string + redirectDisabled bool + } + tests := []struct { + name string + fields fields + want slog.Value + }{ + { + name: "parent", + fields: fields{ + Parent: io.EOF, + }, + want: slog.GroupValue(slog.Any("parent", io.EOF)), + }, + { + name: "description", + fields: fields{ + Description: "oops", + }, + want: slog.GroupValue(slog.String("description", "oops")), + }, + { + name: "errorType", + fields: fields{ + ErrorType: ExpiredToken, + }, + want: slog.GroupValue(slog.String("type", string(ExpiredToken))), + }, + { + name: "state", + fields: fields{ + State: "123", + }, + want: slog.GroupValue(slog.String("state", "123")), + }, + { + name: "all fields", + fields: fields{ + Parent: io.EOF, + Description: "oops", + ErrorType: ExpiredToken, + State: "123", + }, + want: slog.GroupValue( + slog.Any("parent", io.EOF), + slog.String("description", "oops"), + slog.String("type", string(ExpiredToken)), + slog.String("state", "123"), + ), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Error{ + Parent: tt.fields.Parent, + ErrorType: tt.fields.ErrorType, + Description: tt.fields.Description, + State: tt.fields.State, + redirectDisabled: tt.fields.redirectDisabled, + } + got := e.LogValue() + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 7058ebc..d570e25 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "log/slog" "net" "net/http" "net/url" @@ -14,7 +15,6 @@ import ( httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" str "github.com/zitadel/oidc/v3/pkg/strings" - "golang.org/x/exp/slog" ) type AuthRequest interface { diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index 18880f0..2e7c75c 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "io" + "log/slog" "net/http" "net/http/httptest" "net/url" @@ -20,7 +21,6 @@ import ( "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/oidc/v3/pkg/op/mock" "github.com/zitadel/schema" - "golang.org/x/exp/slog" ) func TestAuthorize(t *testing.T) { diff --git a/pkg/op/error.go b/pkg/op/error.go index e4580f6..44b1798 100644 --- a/pkg/op/error.go +++ b/pkg/op/error.go @@ -4,11 +4,11 @@ import ( "context" "errors" "fmt" + "log/slog" "net/http" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" - "golang.org/x/exp/slog" ) type ErrAuthRequest interface { diff --git a/pkg/op/error_test.go b/pkg/op/error_test.go index 50a9cbf..170039c 100644 --- a/pkg/op/error_test.go +++ b/pkg/op/error_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "log/slog" "net/http" "net/http/httptest" "net/url" @@ -14,7 +15,6 @@ import ( "github.com/stretchr/testify/require" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/schema" - "golang.org/x/exp/slog" ) func TestAuthRequestError(t *testing.T) { diff --git a/pkg/op/mock/authorizer.mock.go b/pkg/op/mock/authorizer.mock.go index e4297cb..c7703f1 100644 --- a/pkg/op/mock/authorizer.mock.go +++ b/pkg/op/mock/authorizer.mock.go @@ -6,12 +6,12 @@ package mock import ( context "context" + slog "log/slog" reflect "reflect" gomock "github.com/golang/mock/gomock" http "github.com/zitadel/oidc/v3/pkg/http" op "github.com/zitadel/oidc/v3/pkg/op" - slog "golang.org/x/exp/slog" ) // MockAuthorizer is a mock of Authorizer interface. diff --git a/pkg/op/op.go b/pkg/op/op.go index 14c5356..326737a 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -3,6 +3,7 @@ package op import ( "context" "fmt" + "log/slog" "net/http" "time" @@ -12,7 +13,6 @@ import ( "github.com/zitadel/schema" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" - "golang.org/x/exp/slog" "golang.org/x/text/language" httphelper "github.com/zitadel/oidc/v3/pkg/http" @@ -114,8 +114,6 @@ type OpenIDProvider interface { Crypto() Crypto DefaultLogoutRedirectURI() string Probes() []ProbesFn - - // EXPERIMENTAL: Will change to log/slog import after we drop support for Go 1.20 Logger() *slog.Logger // Deprecated: Provider now implements http.Handler directly. @@ -646,8 +644,6 @@ func WithCORSOptions(opts *cors.Options) Option { } // WithLogger lets a logger other than slog.Default(). -// -// EXPERIMENTAL: Will change to log/slog import after we drop support for Go 1.20 func WithLogger(logger *slog.Logger) Option { return func(o *Provider) error { o.logger = logger diff --git a/pkg/op/server_http.go b/pkg/op/server_http.go index 2220e44..0a5e469 100644 --- a/pkg/op/server_http.go +++ b/pkg/op/server_http.go @@ -2,6 +2,7 @@ package op import ( "context" + "log/slog" "net/http" "net/url" @@ -11,7 +12,6 @@ import ( httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/schema" - "golang.org/x/exp/slog" ) // RegisterServer registers an implementation of Server. diff --git a/pkg/op/server_http_test.go b/pkg/op/server_http_test.go index 6cb268f..9ff07bc 100644 --- a/pkg/op/server_http_test.go +++ b/pkg/op/server_http_test.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "io" + "log/slog" "net/http" "net/http/httptest" "net/url" @@ -19,7 +20,6 @@ import ( httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/schema" - "golang.org/x/exp/slog" ) func TestRegisterServer(t *testing.T) { diff --git a/pkg/op/session.go b/pkg/op/session.go index c933659..6af7d7c 100644 --- a/pkg/op/session.go +++ b/pkg/op/session.go @@ -3,13 +3,13 @@ package op import ( "context" "errors" + "log/slog" "net/http" "net/url" "path" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" - "golang.org/x/exp/slog" ) type SessionEnder interface { diff --git a/pkg/op/token_request.go b/pkg/op/token_request.go index f00b294..2006725 100644 --- a/pkg/op/token_request.go +++ b/pkg/op/token_request.go @@ -2,12 +2,12 @@ package op import ( "context" + "log/slog" "net/http" "net/url" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" - "golang.org/x/exp/slog" ) type Exchanger interface { From 7bac3c6f40883d5cfe64fd35c383d0f039313e34 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 08:10:55 +0100 Subject: [PATCH 358/502] chore(deps): bump github.com/stretchr/testify from 1.8.4 to 1.9.0 (#560) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.4 to 1.9.0. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.8.4...v1.9.0) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2ae42cb..f84e9a5 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/muhlemmer/httpforwarded v0.1.0 github.com/rs/cors v1.10.1 github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/zitadel/logging v0.6.0 github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.24.0 diff --git a/go.sum b/go.sum index 42363b9..6634e85 100644 --- a/go.sum +++ b/go.sum @@ -53,8 +53,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zitadel/logging v0.6.0 h1:t5Nnt//r+m2ZhhoTmoPX+c96pbMarqJvW1Vq6xFTank= From fc743a69c7725b467ef0ca29182fd3255b8b9ca9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Mar 2024 11:07:48 +0100 Subject: [PATCH 359/502] chore(deps): bump golang.org/x/oauth2 from 0.17.0 to 0.18.0 (#562) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.17.0 to 0.18.0. - [Commits](https://github.com/golang/oauth2/compare/v0.17.0...v0.18.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 14 ++++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index f84e9a5..9cb99e6 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.24.0 go.opentelemetry.io/otel/trace v1.24.0 - golang.org/x/oauth2 v0.17.0 + golang.org/x/oauth2 v0.18.0 golang.org/x/text v0.14.0 ) @@ -32,9 +32,9 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect - golang.org/x/crypto v0.19.0 // indirect - golang.org/x/net v0.21.0 // indirect - golang.org/x/sys v0.17.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/sys v0.18.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 6634e85..9b2c86f 100644 --- a/go.sum +++ b/go.sum @@ -70,8 +70,9 @@ go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -85,11 +86,11 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= -golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= +golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= +golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -105,8 +106,9 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= From 5ef597b1dbfb0ddd797b5ccda78edccd2f58d172 Mon Sep 17 00:00:00 2001 From: Ayato Date: Tue, 5 Mar 2024 22:04:43 +0900 Subject: [PATCH 360/502] feat(op): Add response_mode: form_post (#551) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(op): Add response_mode: form_post * Fix to parse the template ahead of time * Fix to render the template in a buffer * Remove unnecessary import * Fix test * Fix example client setting * Make sure the client not to reuse the content of the response * Fix error handling * Add the response_mode param * Allow implicit flow in the example app * feat(rp): allow form_post in code exchange callback handler --------- Co-authored-by: Tim Möhlmann --- example/client/app/app.go | 15 +++++++- example/server/storage/client.go | 4 +-- example/server/storage/oidc.go | 4 ++- pkg/client/rp/relying_party.go | 7 ++-- pkg/oidc/authorization.go | 1 + pkg/op/auth_request.go | 62 ++++++++++++++++++++++++++++++++ pkg/op/auth_request_test.go | 35 ++++++++++++++++-- pkg/op/form_post.html.tmpl | 14 ++++++++ 8 files changed, 131 insertions(+), 11 deletions(-) create mode 100644 pkg/op/form_post.html.tmpl diff --git a/example/client/app/app.go b/example/client/app/app.go index a779169..9b43b8d 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -32,6 +32,7 @@ func main() { issuer := os.Getenv("ISSUER") port := os.Getenv("PORT") scopes := strings.Split(os.Getenv("SCOPES"), " ") + responseMode := os.Getenv("RESPONSE_MODE") redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath) cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure()) @@ -77,12 +78,24 @@ func main() { return uuid.New().String() } + urlOptions := []rp.URLParamOpt{ + rp.WithPromptURLParam("Welcome back!"), + } + + if responseMode != "" { + urlOptions = append(urlOptions, rp.WithResponseModeURLParam(oidc.ResponseMode(responseMode))) + } + // register the AuthURLHandler at your preferred path. // the AuthURLHandler creates the auth request and redirects the user to the auth server. // including state handling with secure cookie and the possibility to use PKCE. // Prompts can optionally be set to inform the server of // any messages that need to be prompted back to the user. - http.Handle("/login", rp.AuthURLHandler(state, provider, rp.WithPromptURLParam("Welcome back!"))) + http.Handle("/login", rp.AuthURLHandler( + state, + provider, + urlOptions..., + )) // for demonstration purposes the returned userinfo response is written as JSON object onto response marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) { diff --git a/example/server/storage/client.go b/example/server/storage/client.go index 2e57cc5..010b9ce 100644 --- a/example/server/storage/client.go +++ b/example/server/storage/client.go @@ -184,10 +184,10 @@ func WebClient(id, secret string, redirectURIs ...string) *Client { applicationType: op.ApplicationTypeWeb, authMethod: oidc.AuthMethodBasic, loginURL: defaultLoginURL, - responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode}, + responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode, oidc.ResponseTypeIDTokenOnly, oidc.ResponseTypeIDToken}, grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken, oidc.GrantTypeTokenExchange}, accessTokenType: op.AccessTokenTypeBearer, - devMode: false, + devMode: true, idTokenUserinfoClaimsAssertion: false, clockSkew: 0, } diff --git a/example/server/storage/oidc.go b/example/server/storage/oidc.go index 2509f77..9cd08d9 100644 --- a/example/server/storage/oidc.go +++ b/example/server/storage/oidc.go @@ -35,6 +35,7 @@ type AuthRequest struct { UserID string Scopes []string ResponseType oidc.ResponseType + ResponseMode oidc.ResponseMode Nonce string CodeChallenge *OIDCCodeChallenge @@ -100,7 +101,7 @@ func (a *AuthRequest) GetResponseType() oidc.ResponseType { } func (a *AuthRequest) GetResponseMode() oidc.ResponseMode { - return "" // we won't handle response mode in this example + return a.ResponseMode } func (a *AuthRequest) GetScopes() []string { @@ -154,6 +155,7 @@ func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthReques UserID: userID, Scopes: authReq.Scopes, ResponseType: authReq.ResponseType, + ResponseMode: authReq.ResponseMode, Nonce: authReq.Nonce, CodeChallenge: &OIDCCodeChallenge{ Challenge: authReq.CodeChallenge, diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 72270fe..74da71e 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -494,9 +494,8 @@ func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp R unauthorizedError(w, r, "failed to get state: "+err.Error(), state, rp) return } - params := r.URL.Query() - if params.Get("error") != "" { - rp.ErrorHandler()(w, r, params.Get("error"), params.Get("error_description"), state) + if errValue := r.FormValue("error"); errValue != "" { + rp.ErrorHandler()(w, r, errValue, r.FormValue("error_description"), state) return } codeOpts := make([]CodeExchangeOpt, len(urlParam)) @@ -521,7 +520,7 @@ func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp R } codeOpts = append(codeOpts, WithClientAssertionJWT(assertion)) } - tokens, err := CodeExchange[C](r.Context(), params.Get("code"), rp, codeOpts...) + tokens, err := CodeExchange[C](r.Context(), r.FormValue("code"), rp, codeOpts...) if err != nil { unauthorizedError(w, r, "failed to exchange token: "+err.Error(), state, rp) return diff --git a/pkg/oidc/authorization.go b/pkg/oidc/authorization.go index 89139ba..fa37dbf 100644 --- a/pkg/oidc/authorization.go +++ b/pkg/oidc/authorization.go @@ -48,6 +48,7 @@ const ( ResponseModeQuery ResponseMode = "query" ResponseModeFragment ResponseMode = "fragment" + ResponseModeFormPost ResponseMode = "form_post" // PromptNone (`none`) disallows the Authorization Server to display any authentication or consent user interface pages. // An error (login_required, interaction_required, ...) will be returned if the user is not already authenticated or consent is needed diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index d570e25..18d8826 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -1,9 +1,12 @@ package op import ( + "bytes" "context" + _ "embed" "errors" "fmt" + "html/template" "log/slog" "net" "net/http" @@ -464,6 +467,17 @@ func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthReques Code: code, State: authReq.GetState(), } + + if authReq.GetResponseMode() == oidc.ResponseModeFormPost { + err := AuthResponseFormPost(w, authReq.GetRedirectURI(), &codeResponse, authorizer.Encoder()) + if err != nil { + AuthRequestError(w, r, authReq, err, authorizer) + return + } + + return + } + callback, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), &codeResponse, authorizer.Encoder()) if err != nil { AuthRequestError(w, r, authReq, err, authorizer) @@ -484,6 +498,17 @@ func AuthResponseToken(w http.ResponseWriter, r *http.Request, authReq AuthReque AuthRequestError(w, r, authReq, err, authorizer) return } + + if authReq.GetResponseMode() == oidc.ResponseModeFormPost { + err := AuthResponseFormPost(w, authReq.GetRedirectURI(), resp, authorizer.Encoder()) + if err != nil { + AuthRequestError(w, r, authReq, err, authorizer) + return + } + + return + } + callback, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), resp, authorizer.Encoder()) if err != nil { AuthRequestError(w, r, authReq, err, authorizer) @@ -535,6 +560,43 @@ func AuthResponseURL(redirectURI string, responseType oidc.ResponseType, respons return mergeQueryParams(uri, params), nil } +//go:embed form_post.html.tmpl +var formPostHtmlTemplate string + +var formPostTmpl = template.Must(template.New("form_post").Parse(formPostHtmlTemplate)) + +// AuthResponseFormPost responds a html page that automatically submits the form which contains the auth response parameters +func AuthResponseFormPost(res http.ResponseWriter, redirectURI string, response any, encoder httphelper.Encoder) error { + values := make(map[string][]string) + err := encoder.Encode(response, values) + if err != nil { + return oidc.ErrServerError().WithParent(err) + } + + params := &struct { + RedirectURI string + Params any + }{ + RedirectURI: redirectURI, + Params: values, + } + + var buf bytes.Buffer + err = formPostTmpl.Execute(&buf, params) + if err != nil { + return oidc.ErrServerError().WithParent(err) + } + + res.Header().Set("Cache-Control", "no-store") + res.WriteHeader(http.StatusOK) + _, err = buf.WriteTo(res) + if err != nil { + return oidc.ErrServerError().WithParent(err) + } + + return nil +} + func setFragment(uri *url.URL, params url.Values) string { uri.Fragment = params.Encode() return uri.String() diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index 2e7c75c..76cb00d 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -1027,9 +1027,10 @@ func TestAuthResponseCode(t *testing.T) { authorizer func(*testing.T) op.Authorizer } type res struct { - wantCode int - wantLocationHeader string - wantBody string + wantCode int + wantLocationHeader string + wantCacheControlHeader string + wantBody string } tests := []struct { name string @@ -1111,6 +1112,33 @@ func TestAuthResponseCode(t *testing.T) { wantBody: "", }, }, + { + name: "success form_post", + args: args{ + authReq: &storage.AuthRequest{ + ID: "id1", + CallbackURI: "https://example.com/callback", + TransferState: "state1", + ResponseMode: "form_post", + }, + authorizer: func(t *testing.T) op.Authorizer { + ctrl := gomock.NewController(t) + storage := mock.NewMockStorage(ctrl) + storage.EXPECT().SaveAuthCode(context.Background(), "id1", "id1") + + authorizer := mock.NewMockAuthorizer(ctrl) + authorizer.EXPECT().Storage().Return(storage) + authorizer.EXPECT().Crypto().Return(&mockCrypto{}) + authorizer.EXPECT().Encoder().Return(schema.NewEncoder()) + return authorizer + }, + }, + res: res{ + wantCode: http.StatusOK, + wantCacheControlHeader: "no-store", + wantBody: "\n\n\n\n
\n\n\n\n\n\n\n
\n\n", + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1121,6 +1149,7 @@ func TestAuthResponseCode(t *testing.T) { defer resp.Body.Close() assert.Equal(t, tt.res.wantCode, resp.StatusCode) assert.Equal(t, tt.res.wantLocationHeader, resp.Header.Get("Location")) + assert.Equal(t, tt.res.wantCacheControlHeader, resp.Header.Get("Cache-Control")) body, err := io.ReadAll(resp.Body) require.NoError(t, err) assert.Equal(t, tt.res.wantBody, string(body)) diff --git a/pkg/op/form_post.html.tmpl b/pkg/op/form_post.html.tmpl new file mode 100644 index 0000000..7bc9ab3 --- /dev/null +++ b/pkg/op/form_post.html.tmpl @@ -0,0 +1,14 @@ + + + + +
+{{with .Params.state}}{{end}} +{{with .Params.code}}{{end}} +{{with .Params.id_token}}{{end}} +{{with .Params.access_token}}{{end}} +{{with .Params.token_type}}{{end}} +{{with .Params.expires_in}}{{end}} +
+ + \ No newline at end of file From e3e48882dfd387a1d2e552ec74890c5946263191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 5 Mar 2024 15:09:14 +0200 Subject: [PATCH 361/502] chore: upgrade to v3 guide (#463) * chore: upgrade to v3 guide first version with sed scripts. * tidy up introduction info * process feedback from @muir * logging chapter * server interface chapter * update readme with v3 badges and link to update guide * resolve comments --- README.md | 9 +- UPGRADING.md | 370 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 377 insertions(+), 2 deletions(-) create mode 100644 UPGRADING.md diff --git a/README.md b/README.md index 72ae8d4..01d7d47 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![Release](https://github.com/zitadel/oidc/workflows/Release/badge.svg)](https://github.com/zitadel/oidc/actions) -[![GoDoc](https://godoc.org/github.com/zitadel/oidc?status.png)](https://pkg.go.dev/github.com/zitadel/oidc) +[![Go Reference](https://pkg.go.dev/badge/github.com/zitadel/oidc/v3.svg)](https://pkg.go.dev/github.com/zitadel/oidc/v3) [![license](https://badgen.net/github/license/zitadel/oidc/)](https://github.com/zitadel/oidc/blob/master/LICENSE) [![release](https://badgen.net/github/release/zitadel/oidc/stable)](https://github.com/zitadel/oidc/releases) -[![Go Report Card](https://goreportcard.com/badge/github.com/zitadel/oidc)](https://goreportcard.com/report/github.com/zitadel/oidc) +[![Go Report Card](https://goreportcard.com/badge/github.com/zitadel/oidc/v3)](https://goreportcard.com/report/github.com/zitadel/oidc/v3) [![codecov](https://codecov.io/gh/zitadel/oidc/branch/main/graph/badge.svg)](https://codecov.io/gh/zitadel/oidc) [![openid_certified](https://cloud.githubusercontent.com/assets/1454075/7611268/4d19de32-f97b-11e4-895b-31b2455a7ca6.png)](https://openid.net/certification/) @@ -37,6 +37,11 @@ The most important packages of the library: /server examples of an OpenID Provider implementations (including dynamic) with some very basic login UI + +### Semver + +This package uses [semver](https://semver.org/) for [releases](https://github.com/zitadel/oidc/releases). Major releases ship breaking changes. Starting with the `v2` to `v3` increment we provide an [upgrade guide](UPGRADING.md) to ease migration to a newer version. + ## How To Use It Check the `/example` folder where example code for different scenarios is located. diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000..6b5a41d --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,370 @@ +# Upgrading + +All commands are executed from the root of the project that imports oidc packages. +`sed` commands are created with **GNU sed** in mind and might need alternate syntax +on non-GNU systems, such as MacOS. +Alternatively, GNU sed can be installed on such systems. (`coreutils` package?). + +## V2 to V3 + +**TL;DR** at the [bottom](#full-script) of this chapter is a full `sed` script +containing all automatic steps at once. + + +As first steps we will: +1. Download the latest v3 module; +2. Replace imports in all Go files; +3. Tidy the module file; + +```bash +go get -u github.com/zitadel/oidc/v3 +find . -type f -name '*.go' | xargs sed -i \ + -e 's/github\.com\/zitadel\/oidc\/v2/github.com\/zitadel\/oidc\/v3/g' +go mod tidy +``` + +### global + +#### go-jose package + +`gopkg.in/square/go-jose.v2` import has been changed to `github.com/go-jose/go-jose/v3`. +That means that the imported types are also changed and imports need to be adapted. + +```bash +find . -type f -name '*.go' | xargs sed -i \ + -e 's/gopkg.in\/square\/go-jose\.v2/github.com\/go-jose\/go-jose\/v3/g' +go mod tidy +``` + +### op + +```go +import "github.com/zitadel/oidc/v3/pkg/op" +``` + +#### Logger + +This version of OIDC adds logging to the framework. For this we use the new Go standard library `log/slog`. (Until v3.12.0 we used `x/exp/slog`). +Mostly OIDC will use error level logs where it's returning an error through a HTTP handler. OIDC errors that are user facing don't carry much context, also for security reasons. With logging we are now able to print the error context, so that developers can more easily find the source of their issues. Previously we just discarded such context. + +Most users of the OP package with the storage interface will not experience breaking changes. However if you use `RequestError()` directly in your code, you now need to give it a `Logger` as final argument. + +The `OpenIDProvider` and sub-interfaces like `Authorizer` and `Exchanger` got a `Logger()` method to return the configured logger. This logger is in turn used by `AuthRequestError()`. You configure the logger with the `WithLogger()` for the `Provider`. By default the `slog.Default()` is used. + +We also provide a new optional interface: [`LogAuthRequest`](https://pkg.go.dev/github.com/zitadel/oidc/v3/pkg/op#LogAuthRequest). If an `AuthRequest` implements this interface, it is completely passed into the logger after an error. Its `LogValue()` will be used by `slog` to print desired fields. This allows omitting sensitive fields you wish not no print. If the interface is not implemented, no `AuthRequest` details will ever be printed. + +#### Server interface + +We've added a new [`Server`](https://pkg.go.dev/github.com/zitadel/oidc/v3/pkg/op#Server) interface. This interface is experimental and subject to change. See [issue 440](https://github.com/zitadel/oidc/issues/440) for the motivation and discussion around this new interface. +Usage of the new interface is not required, but may be used for advanced scenarios when working with the `Storage` interface isn't the optimal solution for your app (like we experienced in [Zitadel](https://github.com/zitadel/zitadel)). + +#### AuthRequestError + +`AuthRequestError` now takes the complete `Authorizer` as final argument, instead of only the encoder. +This is to facilitate the use of the `Logger` as described above. + +```bash +find . -type f -name '*.go' | xargs sed -i \ + -e 's/\bAuthRequestError(w, r, authReq, err, authorizer.Encoder())/AuthRequestError(w, r, authReq, err, authorizer)/g' +``` + +Note: the sed regex might not find all uses if the local variables of the passed arguments use different names. + +#### AccessTokenVerifier + +`AccessTokenVerifier` interface has become a struct type. `NewAccessTokenVerifier` now returns a pointer to `AccessTokenVerifier`. +Variable and struct fields declarations need to be changed from `op.AccessTokenVerifier` to `*op.AccessTokenVerifier`. + +```bash +find . -type f -name '*.go' | xargs sed -i \ + -e 's/\bop\.AccessTokenVerifier\b/*op.AccessTokenVerifier/g' +``` + +#### JWTProfileVerifier + +`JWTProfileVerifier` interface has become a struct type. `NewJWTProfileVerifier` now returns a pointer to `JWTProfileVerifier`. +Variable and struct fields declarations need to be changed from `op.JWTProfileVerifier` to `*op.JWTProfileVerifier`. + +```bash +find . -type f -name '*.go' | xargs sed -i \ + -e 's/\bop\.JWTProfileVerifier\b/*op.JWTProfileVerifier/g' +``` + +#### IDTokenHintVerifier + +`IDTokenHintVerifier` interface has become a struct type. `NewIDTokenHintVerifier` now returns a pointer to `IDTokenHintVerifier`. +Variable and struct fields declarations need to be changed from `op.IDTokenHintVerifier` to `*op.IDTokenHintVerifier`. + +```bash +find . -type f -name '*.go' | xargs sed -i \ + -e 's/\bop\.IDTokenHintVerifier\b/*op.IDTokenHintVerifier/g' +``` + +#### ParseRequestObject + +`ParseRequestObject` no longer returns `*oidc.AuthRequest` as it already operates on the pointer for the passed `authReq` argument. As such the argument and the return value were the same pointer. Callers can just use the original `*oidc.AuthRequest` now. + +#### Endpoint Configuration + +`Endpoint`s returned from `Configuration` interface methods are now pointers. Usually, `op.Provider` is the main implementation of the `Configuration` interface. However, if a custom implementation is used, you should be able to update it using the following: + +```bash +find . -type f -name '*.go' | xargs sed -i \ + -e 's/AuthorizationEndpoint() Endpoint/AuthorizationEndpoint() *Endpoint/g' \ + -e 's/TokenEndpoint() Endpoint/TokenEndpoint() *Endpoint/g' \ + -e 's/IntrospectionEndpoint() Endpoint/IntrospectionEndpoint() *Endpoint/g' \ + -e 's/UserinfoEndpoint() Endpoint/UserinfoEndpoint() *Endpoint/g' \ + -e 's/RevocationEndpoint() Endpoint/RevocationEndpoint() *Endpoint/g' \ + -e 's/EndSessionEndpoint() Endpoint/EndSessionEndpoint() *Endpoint/g' \ + -e 's/KeysEndpoint() Endpoint/KeysEndpoint() *Endpoint/g' \ + -e 's/DeviceAuthorizationEndpoint() Endpoint/DeviceAuthorizationEndpoint() *Endpoint/g' +``` + +#### CreateDiscoveryConfig + +`CreateDiscoveryConfig` now takes a context as first argument. The following adds `context.TODO()` to the function: + +```bash +find . -type f -name '*.go' | xargs sed -i \ + -e 's/op\.CreateDiscoveryConfig(/op.CreateDiscoveryConfig(context.TODO(), /g' +``` + +It now takes the issuer out of the context using the [`IssuerFromContext`](https://pkg.go.dev/github.com/zitadel/oidc/v3/pkg/op#IssuerFromContext) functionality, +instead of the `config.IssuerFromRequest()` method. + +#### CreateRouter + +`CreateRouter` now returns a `chi.Router` instead of `*mux.Router`. +Usually this function is called when the Provider is constructed and not by package consumers. +However if your project does call this function directly, manual update of the code is required. + +#### DeviceAuthorizationStorage + +`DeviceAuthorizationStorage` dropped the following methods: + +- `GetDeviceAuthorizationByUserCode` +- `CompleteDeviceAuthorization` +- `DenyDeviceAuthorization` + +These methods proved not to be required from a library point of view. +Implementations of a device authorization flow may take care of these calls in a way they see fit. + +#### AuthorizeCodeChallenge + +The `AuthorizeCodeChallenge` function now only takes the `CodeVerifier` argument, instead of the complete `*oidc.AccessTokenRequest`. + +```bash +find . -type f -name '*.go' | xargs sed -i \ + -e 's/op\.AuthorizeCodeChallenge(tokenReq/op.AuthorizeCodeChallenge(tokenReq.CodeVerifier/g' +``` + +### client + +```go +import "github.com/zitadel/oidc/v3/pkg/client" +``` + +#### Context + +All client calls now take a context as first argument. The following adds `context.TODO()` to all the affected functions: + +```bash +find . -type f -name '*.go' | xargs sed -i \ + -e 's/client\.Discover(/client.Discover(context.TODO(), /g' \ + -e 's/client\.CallTokenEndpoint(/client.CallTokenEndpoint(context.TODO(), /g' \ + -e 's/client\.CallEndSessionEndpoint(/client.CallEndSessionEndpoint(context.TODO(), /g' \ + -e 's/client\.CallRevokeEndpoint(/client.CallRevokeEndpoint(context.TODO(), /g' \ + -e 's/client\.CallTokenExchangeEndpoint(/client.CallTokenExchangeEndpoint(context.TODO(), /g' \ + -e 's/client\.CallDeviceAuthorizationEndpoint(/client.CallDeviceAuthorizationEndpoint(context.TODO(), /g' \ + -e 's/client\.JWTProfileExchange(/client.JWTProfileExchange(context.TODO(), /g' +``` + +#### keyFile type + +The `keyFile` struct type is now exported a `KeyFile` and returned by the `ConfigFromKeyFile` and `ConfigFromKeyFileData`. No changes are needed on the caller's side. + +### client/profile + +The package now defines a new interface `TokenSource` which compliments the `oauth2.TokenSource` with a `TokenCtx` method, so that a context can be explicitly added on each call. Users can migrate to the new method when they whish. + +`NewJWTProfileTokenSource` now takes a context as first argument, so do the related `NewJWTProfileTokenSourceFromKeyFile` and `NewJWTProfileTokenSourceFromKeyFileData`. The context is used for the Discovery request. + +```bash +find . -type f -name '*.go' | xargs sed -i \ + -e 's/profile\.NewJWTProfileTokenSource(/profile.NewJWTProfileTokenSource(context.TODO(), /g' \ + -e 's/profile\.NewJWTProfileTokenSourceFromKeyFileData(/profile.NewJWTProfileTokenSourceFromKeyFileData(context.TODO(), /g' \ + -e 's/profile\.NewJWTProfileTokenSourceFromKeyFile(/profile.NewJWTProfileTokenSourceFromKeyFile(context.TODO(), /g' +``` + + +### client/rp + +```go +import "github.com/zitadel/oidc/v3/pkg/client/rs" +``` + +#### Discover + +The `Discover` function has been removed. Use `client.Discover` instead. + +#### Context + +Most `rp` functions now require a context as first argument. The following adds `context.TODO()` to the function that have no additional changes. Functions with more complex changes are documented below. + +```bash +find . -type f -name '*.go' | xargs sed -i \ + -e 's/rp\.NewRelyingPartyOIDC(/rp.NewRelyingPartyOIDC(context.TODO(), /g' \ + -e 's/rp\.EndSession(/rp.EndSession(context.TODO(), /g' \ + -e 's/rp\.RevokeToken(/rp.RevokeToken(context.TODO(), /g' \ + -e 's/rp\.DeviceAuthorization(/rp.DeviceAuthorization(context.TODO(), /g' +``` + +Remember to replace `context.TODO()` with a context that is applicable for your app, where possible. + +#### RefreshAccessToken + +1. Renamed to `RefreshTokens`; +2. A context must be passed; +3. An `*oidc.Tokens` object is now returned, which included an ID Token if it was returned by the server; +4. The function is now generic and requires a type argument for the `IDTokenClaims` implementation inside the returned `oidc.Tokens` object; + +For most use cases `*oidc.IDTokenClaims` can be used as type argument. A custom implementation of `oidc.IDClaims` can be used if type-safe access to custom claims is required. + +```bash +find . -type f -name '*.go' | xargs sed -i \ + -e 's/rp\.RefreshAccessToken(/rp.RefreshTokens[*oidc.IDTokenClaims](context.TODO(), /g' +``` + +Users that called `tokens.Extra("id_token").(string)` and a subsequent `VerifyTokens` to get the claims, no longer need to do this. The ID token is verified (when present) by `RefreshTokens` already. + + +#### Userinfo + +1. A context must be passed as first argument; +2. The function is now generic and requires a type argument for the returned user info object; + +For most use cases `*oidc.UserInfo` can be used a type argument. A [custom implementation](https://pkg.go.dev/github.com/zitadel/oidc/v3/pkg/client/rp#example-Userinfo-Custom) of `rp.SubjectGetter` can be used if type-safe access to custom claims is required. + +```bash +find . -type f -name '*.go' | xargs sed -i \ + -e 's/rp\.Userinfo(/rp.Userinfo[*oidc.UserInfo](context.TODO(), /g' +``` + +#### UserinfoCallback + +`UserinfoCallback` has an additional type argument fot the `UserInfo` object. Typically the type argument can be inferred by the compiler, by the function that is passed. The actual code update cannot be done by a simple `sed` script and depends on how the caller implemented the function. + + +#### IDTokenVerifier + +`IDTokenVerifier` interface has become a struct type. `NewIDTokenVerifier` now returns a pointer to `IDTokenVerifier`. +Variable and struct fields declarations need to be changed from `rp.IDTokenVerifier` to `*rp.AccessTokenVerifier`. + +```bash +find . -type f -name '*.go' | xargs sed -i \ + -e 's/\brp\.IDTokenVerifier\b/*rp.IDTokenVerifier/g' +``` + +### client/rs + +```go +import "github.com/zitadel/oidc/v3/pkg/client/rs" +``` + +#### NewResourceServer + +The `NewResourceServerClientCredentials` and `NewResourceServerJWTProfile` constructor functions now take a context as first argument. + +```bash +find . -type f -name '*.go' | xargs sed -i \ + -e 's/rs\.NewResourceServerClientCredentials(/rs.NewResourceServerClientCredentials(context.TODO(), /g' \ + -e 's/rs\.NewResourceServerJWTProfile(/rs.NewResourceServerJWTProfile(context.TODO(), /g' +``` + +#### Introspect + +`Introspect` is now generic and requires a type argument for the returned introspection response. For most use cases `*oidc.IntrospectionResponse` can be used as type argument. Any other response type if type-safe access to [custom claims](https://pkg.go.dev/github.com/zitadel/oidc/v3/pkg/client/rs#example-Introspect-Custom) is required. + +```bash +find . -type f -name '*.go' | xargs sed -i \ + -e 's/rs\.Introspect(/rs.Introspect[*oidc.IntrospectionResponse](/g' +``` + +### client/tokenexchange + +The `TokenExchanger` constructor functions `NewTokenExchanger` and `NewTokenExchangerClientCredentials` now take a context as first argument. +As well as the `ExchangeToken` function. + +```bash +find . -type f -name '*.go' | xargs sed -i \ + -e 's/tokenexchange\.NewTokenExchanger(/tokenexchange.NewTokenExchanger(context.TODO(), /g' \ + -e 's/tokenexchange\.NewTokenExchangerClientCredentials(/tokenexchange.NewTokenExchangerClientCredentials(context.TODO(), /g' \ + -e 's/tokenexchange\.ExchangeToken(/tokenexchange.ExchangeToken(context.TODO(), /g' +``` + +### oidc + +#### SpaceDelimitedArray + +The `SpaceDelimitedArray` type's `Encode()` function has been renamed to `String()` so it implements the `fmt.Stringer` interface. If the `Encode` method was called by a package consumer, it should be changed manually. + +#### Verifier + +The `Verifier` interface as been changed into a struct type. The struct type is aliased in the `op` and `rp` packages for the specific token use cases. See the relevant section above. + +### Full script + +For the courageous this is the full `sed` script which combines all the steps described above. +It should migrate most of the code in a repository to a more-or-less compilable state, +using defaults such as `context.TODO()` where possible. + +Warnings: +- Again, this is written for **GNU sed** not the posix variant. +- Assumes imports that use the package names, not aliases. +- Do this on a project with version control (eg Git), that allows you to rollback if things went wrong. +- The script has been tested on the [ZITADEL](https://github.com/zitadel/zitadel) project, but we do not use all affected symbols. Parts of the script are mere guesswork. + +```bash +go get -u github.com/zitadel/oidc/v3 +find . -type f -name '*.go' | xargs sed -i \ + -e 's/github\.com\/zitadel\/oidc\/v2/github.com\/zitadel\/oidc\/v3/g' \ + -e 's/gopkg.in\/square\/go-jose\.v2/github.com\/go-jose\/go-jose\/v3/g' \ + -e 's/\bAuthRequestError(w, r, authReq, err, authorizer.Encoder())/AuthRequestError(w, r, authReq, err, authorizer)/g' \ + -e 's/\bop\.AccessTokenVerifier\b/*op.AccessTokenVerifier/g' \ + -e 's/\bop\.JWTProfileVerifier\b/*op.JWTProfileVerifier/g' \ + -e 's/\bop\.IDTokenHintVerifier\b/*op.IDTokenHintVerifier/g' \ + -e 's/AuthorizationEndpoint() Endpoint/AuthorizationEndpoint() *Endpoint/g' \ + -e 's/TokenEndpoint() Endpoint/TokenEndpoint() *Endpoint/g' \ + -e 's/IntrospectionEndpoint() Endpoint/IntrospectionEndpoint() *Endpoint/g' \ + -e 's/UserinfoEndpoint() Endpoint/UserinfoEndpoint() *Endpoint/g' \ + -e 's/RevocationEndpoint() Endpoint/RevocationEndpoint() *Endpoint/g' \ + -e 's/EndSessionEndpoint() Endpoint/EndSessionEndpoint() *Endpoint/g' \ + -e 's/KeysEndpoint() Endpoint/KeysEndpoint() *Endpoint/g' \ + -e 's/DeviceAuthorizationEndpoint() Endpoint/DeviceAuthorizationEndpoint() *Endpoint/g' \ + -e 's/op\.CreateDiscoveryConfig(/op.CreateDiscoveryConfig(context.TODO(), /g' \ + -e 's/op\.AuthorizeCodeChallenge(tokenReq/op.AuthorizeCodeChallenge(tokenReq.CodeVerifier/g' \ + -e 's/client\.Discover(/client.Discover(context.TODO(), /g' \ + -e 's/client\.CallTokenEndpoint(/client.CallTokenEndpoint(context.TODO(), /g' \ + -e 's/client\.CallEndSessionEndpoint(/client.CallEndSessionEndpoint(context.TODO(), /g' \ + -e 's/client\.CallRevokeEndpoint(/client.CallRevokeEndpoint(context.TODO(), /g' \ + -e 's/client\.CallTokenExchangeEndpoint(/client.CallTokenExchangeEndpoint(context.TODO(), /g' \ + -e 's/client\.CallDeviceAuthorizationEndpoint(/client.CallDeviceAuthorizationEndpoint(context.TODO(), /g' \ + -e 's/client\.JWTProfileExchange(/client.JWTProfileExchange(context.TODO(), /g' \ + -e 's/profile\.NewJWTProfileTokenSource(/profile.NewJWTProfileTokenSource(context.TODO(), /g' \ + -e 's/profile\.NewJWTProfileTokenSourceFromKeyFileData(/profile.NewJWTProfileTokenSourceFromKeyFileData(context.TODO(), /g' \ + -e 's/profile\.NewJWTProfileTokenSourceFromKeyFile(/profile.NewJWTProfileTokenSourceFromKeyFile(context.TODO(), /g' \ + -e 's/rp\.NewRelyingPartyOIDC(/rp.NewRelyingPartyOIDC(context.TODO(), /g' \ + -e 's/rp\.EndSession(/rp.EndSession(context.TODO(), /g' \ + -e 's/rp\.RevokeToken(/rp.RevokeToken(context.TODO(), /g' \ + -e 's/rp\.DeviceAuthorization(/rp.DeviceAuthorization(context.TODO(), /g' \ + -e 's/rp\.RefreshAccessToken(/rp.RefreshTokens[*oidc.IDTokenClaims](context.TODO(), /g' \ + -e 's/rp\.Userinfo(/rp.Userinfo[*oidc.UserInfo](context.TODO(), /g' \ + -e 's/\brp\.IDTokenVerifier\b/*rp.IDTokenVerifier/g' \ + -e 's/rs\.NewResourceServerClientCredentials(/rs.NewResourceServerClientCredentials(context.TODO(), /g' \ + -e 's/rs\.NewResourceServerJWTProfile(/rs.NewResourceServerJWTProfile(context.TODO(), /g' \ + -e 's/rs\.Introspect(/rs.Introspect[*oidc.IntrospectionResponse](/g' \ + -e 's/tokenexchange\.NewTokenExchanger(/tokenexchange.NewTokenExchanger(context.TODO(), /g' \ + -e 's/tokenexchange\.NewTokenExchangerClientCredentials(/tokenexchange.NewTokenExchangerClientCredentials(context.TODO(), /g' \ + -e 's/tokenexchange\.ExchangeToken(/tokenexchange.ExchangeToken(context.TODO(), /g' +go mod tidy +``` \ No newline at end of file From d18aba8cb336c27a7ddc5146ab97f1afe730ede5 Mon Sep 17 00:00:00 2001 From: adlerhurst Date: Wed, 6 Mar 2024 18:38:37 +0100 Subject: [PATCH 362/502] feat(rp): extend tracing --- pkg/op/auth_request.go | 32 ++++++++++++++++--- pkg/op/client.go | 13 +++++++- pkg/op/device.go | 14 +++++++++ pkg/op/discovery.go | 3 ++ pkg/op/keys.go | 4 +++ pkg/op/server_http.go | 12 ++++++- pkg/op/server_legacy.go | 54 ++++++++++++++++++++++++++++++++ pkg/op/session.go | 7 +++++ pkg/op/token.go | 3 ++ pkg/op/token_exchange.go | 14 +++++++++ pkg/op/token_intospection.go | 4 +++ pkg/op/token_refresh.go | 3 ++ pkg/op/token_request.go | 7 +++++ pkg/op/token_revocation.go | 11 +++++++ pkg/op/userinfo.go | 15 +++++++++ pkg/op/verifier_access_token.go | 3 ++ pkg/op/verifier_id_token_hint.go | 3 ++ pkg/op/verifier_jwt_profile.go | 3 ++ 18 files changed, 198 insertions(+), 7 deletions(-) diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 18d8826..435e3f4 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -70,12 +70,16 @@ func authorizeCallbackHandler(authorizer Authorizer) func(http.ResponseWriter, * // Authorize handles the authorization request, including // parsing, validating, storing and finally redirecting to the login handler func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { + ctx, span := tracer.Start(r.Context(), "Authorize") + r = r.WithContext(ctx) + defer span.End() + authReq, err := ParseAuthorizeRequest(r, authorizer.Decoder()) if err != nil { AuthRequestError(w, r, nil, err, authorizer) return } - ctx := r.Context() + ctx = r.Context() if authReq.RequestParam != "" && authorizer.RequestObjectSupported() { err = ParseRequestObject(ctx, authReq, authorizer.Storage(), IssuerFromContext(ctx)) if err != nil { @@ -210,6 +214,9 @@ func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oi // ValidateAuthRequest validates the authorize parameters and returns the userID of the id_token_hint if passed func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier *IDTokenHintVerifier) (sub string, err error) { + ctx, span := tracer.Start(ctx, "ValidateAuthRequest") + defer span.End() + authReq.MaxAge, err = ValidateAuthReqPrompt(authReq.Prompt, authReq.MaxAge) if err != nil { return "", err @@ -310,7 +317,7 @@ func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.Res return checkURIAgainstRedirects(client, uri) } if client.ApplicationType() == ApplicationTypeNative { - return validateAuthReqRedirectURINative(client, uri, responseType) + return validateAuthReqRedirectURINative(client, uri) } if err := checkURIAgainstRedirects(client, uri); err != nil { return err @@ -330,7 +337,7 @@ func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.Res } // ValidateAuthReqRedirectURINative validates the passed redirect_uri and response_type to the registered uris and client type -func validateAuthReqRedirectURINative(client Client, uri string, responseType oidc.ResponseType) error { +func validateAuthReqRedirectURINative(client Client, uri string) error { parsedURL, isLoopback := HTTPLoopbackOrLocalhost(uri) isCustomSchema := !strings.HasPrefix(uri, "http://") if err := checkURIAgainstRedirects(client, uri); err == nil { @@ -362,8 +369,8 @@ func equalURI(url1, url2 *url.URL) bool { return url1.Path == url2.Path && url1.RawQuery == url2.RawQuery } -func HTTPLoopbackOrLocalhost(rawurl string) (*url.URL, bool) { - parsedURL, err := url.Parse(rawurl) +func HTTPLoopbackOrLocalhost(rawURL string) (*url.URL, bool) { + parsedURL, err := url.Parse(rawURL) if err != nil { return nil, false } @@ -409,6 +416,10 @@ func RedirectToLogin(authReqID string, client Client, w http.ResponseWriter, r * // AuthorizeCallback handles the callback after authentication in the Login UI func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { + ctx, span := tracer.Start(r.Context(), "AuthorizeCallback") + r = r.WithContext(ctx) + defer span.End() + id, err := ParseAuthorizeCallbackRequest(r) if err != nil { AuthRequestError(w, r, nil, err, authorizer) @@ -441,6 +452,10 @@ func ParseAuthorizeCallbackRequest(r *http.Request) (id string, err error) { // AuthResponse creates the successful authentication response (either code or tokens) func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "ValidateAuthRequest") + r = r.WithContext(ctx) + defer span.End() + client, err := authorizer.Storage().GetClientByClientID(r.Context(), authReq.GetClientID()) if err != nil { AuthRequestError(w, r, authReq, err, authorizer) @@ -455,6 +470,10 @@ func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWri // AuthResponseCode creates the successful code authentication response func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer) { + ctx, span := tracer.Start(r.Context(), "AuthResponseCode") + r = r.WithContext(ctx) + defer span.End() + code, err := CreateAuthRequestCode(r.Context(), authReq, authorizer.Storage(), authorizer.Crypto()) if err != nil { AuthRequestError(w, r, authReq, err, authorizer) @@ -519,6 +538,9 @@ func AuthResponseToken(w http.ResponseWriter, r *http.Request, authReq AuthReque // CreateAuthRequestCode creates and stores a code for the auth code response func CreateAuthRequestCode(ctx context.Context, authReq AuthRequest, storage Storage, crypto Crypto) (string, error) { + ctx, span := tracer.Start(ctx, "CreateAuthRequestCode") + defer span.End() + code, err := BuildAuthRequestCode(authReq, crypto) if err != nil { return "", err diff --git a/pkg/op/client.go b/pkg/op/client.go index 0574afa..e0a0443 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -92,6 +92,9 @@ type ClientJWTProfile interface { } func ClientJWTAuth(ctx context.Context, ca oidc.ClientAssertionParams, verifier ClientJWTProfile) (clientID string, err error) { + ctx, span := tracer.Start(ctx, "ClientJWTAuth") + defer span.End() + if ca.ClientAssertion == "" { return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials) } @@ -104,6 +107,10 @@ func ClientJWTAuth(ctx context.Context, ca oidc.ClientAssertionParams, verifier } func ClientBasicAuth(r *http.Request, storage Storage) (clientID string, err error) { + ctx, span := tracer.Start(r.Context(), "ClientBasicAuth") + r = r.WithContext(ctx) + defer span.End() + clientID, clientSecret, ok := r.BasicAuth() if !ok { return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials) @@ -146,6 +153,10 @@ type clientData struct { // If no client id can be obtained by any method, oidc.ErrInvalidClient // is returned with ErrMissingClientID wrapped in it. func ClientIDFromRequest(r *http.Request, p ClientProvider) (clientID string, authenticated bool, err error) { + ctx, span := tracer.Start(r.Context(), "ClientIDFromRequest") + r = r.WithContext(ctx) + defer span.End() + err = r.ParseForm() if err != nil { return "", false, oidc.ErrInvalidRequest().WithDescription("cannot parse form").WithParent(err) @@ -171,7 +182,7 @@ func ClientIDFromRequest(r *http.Request, p ClientProvider) (clientID string, au } // if the client did not send a Basic Auth Header, ignore the `ErrNoClientCredentials` // but return other errors immediately - if err != nil && !errors.Is(err, ErrNoClientCredentials) { + if !errors.Is(err, ErrNoClientCredentials) { return "", false, err } diff --git a/pkg/op/device.go b/pkg/op/device.go index 1b86d04..d08b4fd 100644 --- a/pkg/op/device.go +++ b/pkg/op/device.go @@ -64,6 +64,10 @@ func DeviceAuthorizationHandler(o OpenIDProvider) func(http.ResponseWriter, *htt } func DeviceAuthorization(w http.ResponseWriter, r *http.Request, o OpenIDProvider) error { + ctx, span := tracer.Start(r.Context(), "DeviceAuthorization") + r = r.WithContext(ctx) + defer span.End() + req, err := ParseDeviceCodeRequest(r, o) if err != nil { return err @@ -78,6 +82,9 @@ func DeviceAuthorization(w http.ResponseWriter, r *http.Request, o OpenIDProvide } func createDeviceAuthorization(ctx context.Context, req *oidc.DeviceAuthorizationRequest, clientID string, o OpenIDProvider) (*oidc.DeviceAuthorizationResponse, error) { + ctx, span := tracer.Start(ctx, "createDeviceAuthorization") + defer span.End() + storage, err := assertDeviceStorage(o.Storage()) if err != nil { return nil, err @@ -127,6 +134,10 @@ func createDeviceAuthorization(ctx context.Context, req *oidc.DeviceAuthorizatio } func ParseDeviceCodeRequest(r *http.Request, o OpenIDProvider) (*oidc.DeviceAuthorizationRequest, error) { + ctx, span := tracer.Start(r.Context(), "ParseDeviceCodeRequest") + r = r.WithContext(ctx) + defer span.End() + clientID, _, err := ClientIDFromRequest(r, o) if err != nil { return nil, err @@ -288,6 +299,9 @@ func (r *DeviceAuthorizationState) GetSubject() string { } func CheckDeviceAuthorizationState(ctx context.Context, clientID, deviceCode string, exchanger Exchanger) (*DeviceAuthorizationState, error) { + ctx, span := tracer.Start(ctx, "CheckDeviceAuthorization") + defer span.End() + storage, err := assertDeviceStorage(exchanger.Storage()) if err != nil { return nil, err diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 6af1674..7b5ecbe 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -135,6 +135,9 @@ func SubjectTypes(c Configuration) []string { } func SigAlgorithms(ctx context.Context, storage DiscoverStorage) []string { + ctx, span := tracer.Start(ctx, "SigAlgorithms") + defer span.End() + algorithms, err := storage.SignatureAlgorithms(ctx) if err != nil { return nil diff --git a/pkg/op/keys.go b/pkg/op/keys.go index fe111f0..d55c8d1 100644 --- a/pkg/op/keys.go +++ b/pkg/op/keys.go @@ -20,6 +20,10 @@ func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) { } func Keys(w http.ResponseWriter, r *http.Request, k KeyProvider) { + ctx, span := tracer.Start(r.Context(), "Keys") + r = r.WithContext(ctx) + defer span.End() + keySet, err := k.KeySet(r.Context()) if err != nil { httphelper.MarshalJSONWithStatus(w, err, http.StatusInternalServerError) diff --git a/pkg/op/server_http.go b/pkg/op/server_http.go index 0a5e469..725dd64 100644 --- a/pkg/op/server_http.go +++ b/pkg/op/server_http.go @@ -124,7 +124,13 @@ func (s *webServer) createRouter() { func (s *webServer) endpointRoute(e *Endpoint, hf http.HandlerFunc) { if e != nil { - s.router.HandleFunc(e.Relative(), hf) + traceHandler := func(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), e.Relative()) + r = r.WithContext(ctx) + hf(w, r) + defer span.End() + } + s.router.HandleFunc(e.Relative(), traceHandler) s.logger.Info("registered route", "endpoint", e.Relative()) } } @@ -133,6 +139,10 @@ type clientHandler func(w http.ResponseWriter, r *http.Request, client Client) func (s *webServer) withClient(handler clientHandler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), r.URL.Path) + defer span.End() + r = r.WithContext(ctx) + client, err := s.verifyRequestClient(r) if err != nil { WriteError(w, r, err, s.getLogger(r.Context())) diff --git a/pkg/op/server_legacy.go b/pkg/op/server_legacy.go index f99d15d..6b6d4b3 100644 --- a/pkg/op/server_legacy.go +++ b/pkg/op/server_legacy.go @@ -79,6 +79,9 @@ func (s *LegacyServer) Endpoints() Endpoints { // AuthCallbackURL builds the url for the redirect (with the requestID) after a successful login func (s *LegacyServer) AuthCallbackURL() func(context.Context, string) string { return func(ctx context.Context, requestID string) string { + ctx, span := tracer.Start(ctx, "LegacyServer.AuthCallbackURL") + defer span.End() + return s.endpoints.Authorization.Absolute(IssuerFromContext(ctx)) + authCallbackPathSuffix + "?id=" + requestID } } @@ -98,12 +101,18 @@ func (s *LegacyServer) Ready(ctx context.Context, r *Request[struct{}]) (*Respon } func (s *LegacyServer) Discovery(ctx context.Context, r *Request[struct{}]) (*Response, error) { + ctx, span := tracer.Start(ctx, "LegacyServer.Discovery") + defer span.End() + return NewResponse( createDiscoveryConfigV2(ctx, s.provider, s.provider.Storage(), &s.endpoints), ), nil } func (s *LegacyServer) Keys(ctx context.Context, r *Request[struct{}]) (*Response, error) { + ctx, span := tracer.Start(ctx, "LegacyServer.Keys") + defer span.End() + keys, err := s.provider.Storage().KeySet(ctx) if err != nil { return nil, AsStatusError(err, http.StatusInternalServerError) @@ -117,6 +126,9 @@ var ( ) func (s *LegacyServer) VerifyAuthRequest(ctx context.Context, r *Request[oidc.AuthRequest]) (*ClientRequest[oidc.AuthRequest], error) { + ctx, span := tracer.Start(ctx, "LegacyServer.VerifyAuthRequest") + defer span.End() + if r.Data.RequestParam != "" { if !s.provider.RequestObjectSupported() { return nil, oidc.ErrRequestNotSupported() @@ -141,6 +153,9 @@ func (s *LegacyServer) VerifyAuthRequest(ctx context.Context, r *Request[oidc.Au } func (s *LegacyServer) Authorize(ctx context.Context, r *ClientRequest[oidc.AuthRequest]) (_ *Redirect, err error) { + ctx, span := tracer.Start(ctx, "LegacyServer.Authorize") + defer span.End() + userID, err := ValidateAuthReqIDTokenHint(ctx, r.Data.IDTokenHint, s.provider.IDTokenHintVerifier(ctx)) if err != nil { return nil, err @@ -153,6 +168,9 @@ func (s *LegacyServer) Authorize(ctx context.Context, r *ClientRequest[oidc.Auth } func (s *LegacyServer) DeviceAuthorization(ctx context.Context, r *ClientRequest[oidc.DeviceAuthorizationRequest]) (*Response, error) { + ctx, span := tracer.Start(ctx, "LegacyServer.DeviceAuthorization") + defer span.End() + response, err := createDeviceAuthorization(ctx, r.Data, r.Client.GetID(), s.provider) if err != nil { return nil, AsStatusError(err, http.StatusInternalServerError) @@ -161,6 +179,9 @@ func (s *LegacyServer) DeviceAuthorization(ctx context.Context, r *ClientRequest } func (s *LegacyServer) VerifyClient(ctx context.Context, r *Request[ClientCredentials]) (Client, error) { + ctx, span := tracer.Start(ctx, "LegacyServer.VerifyClient") + defer span.End() + if oidc.GrantType(r.Form.Get("grant_type")) == oidc.GrantTypeClientCredentials { storage, ok := s.provider.Storage().(ClientCredentialsStorage) if !ok { @@ -201,6 +222,9 @@ func (s *LegacyServer) VerifyClient(ctx context.Context, r *Request[ClientCreden } func (s *LegacyServer) CodeExchange(ctx context.Context, r *ClientRequest[oidc.AccessTokenRequest]) (*Response, error) { + ctx, span := tracer.Start(ctx, "LegacyServer.CodeExchange") + defer span.End() + authReq, err := AuthRequestByCode(ctx, s.provider.Storage(), r.Data.Code) if err != nil { return nil, err @@ -221,6 +245,9 @@ func (s *LegacyServer) CodeExchange(ctx context.Context, r *ClientRequest[oidc.A } func (s *LegacyServer) RefreshToken(ctx context.Context, r *ClientRequest[oidc.RefreshTokenRequest]) (*Response, error) { + ctx, span := tracer.Start(ctx, "LegacyServer.RefreshToken") + defer span.End() + if !s.provider.GrantTypeRefreshTokenSupported() { return nil, unimplementedGrantError(oidc.GrantTypeRefreshToken) } @@ -242,6 +269,9 @@ func (s *LegacyServer) RefreshToken(ctx context.Context, r *ClientRequest[oidc.R } func (s *LegacyServer) JWTProfile(ctx context.Context, r *Request[oidc.JWTProfileGrantRequest]) (*Response, error) { + ctx, span := tracer.Start(ctx, "LegacyServer.JWTProfile") + defer span.End() + exchanger, ok := s.provider.(JWTAuthorizationGrantExchanger) if !ok { return nil, unimplementedGrantError(oidc.GrantTypeBearer) @@ -263,6 +293,9 @@ func (s *LegacyServer) JWTProfile(ctx context.Context, r *Request[oidc.JWTProfil } func (s *LegacyServer) TokenExchange(ctx context.Context, r *ClientRequest[oidc.TokenExchangeRequest]) (*Response, error) { + ctx, span := tracer.Start(ctx, "LegacyServer.TokenExchange") + defer span.End() + if !s.provider.GrantTypeTokenExchangeSupported() { return nil, unimplementedGrantError(oidc.GrantTypeTokenExchange) } @@ -278,6 +311,9 @@ func (s *LegacyServer) TokenExchange(ctx context.Context, r *ClientRequest[oidc. } func (s *LegacyServer) ClientCredentialsExchange(ctx context.Context, r *ClientRequest[oidc.ClientCredentialsRequest]) (*Response, error) { + ctx, span := tracer.Start(ctx, "LegacyServer.ClientCredentialsExchange") + defer span.End() + storage, ok := s.provider.Storage().(ClientCredentialsStorage) if !ok { return nil, unimplementedGrantError(oidc.GrantTypeClientCredentials) @@ -294,6 +330,9 @@ func (s *LegacyServer) ClientCredentialsExchange(ctx context.Context, r *ClientR } func (s *LegacyServer) DeviceToken(ctx context.Context, r *ClientRequest[oidc.DeviceAccessTokenRequest]) (*Response, error) { + ctx, span := tracer.Start(ctx, "LegacyServer.DeviceToken") + defer span.End() + if !s.provider.GrantTypeDeviceCodeSupported() { return nil, unimplementedGrantError(oidc.GrantTypeDeviceCode) } @@ -314,6 +353,9 @@ func (s *LegacyServer) DeviceToken(ctx context.Context, r *ClientRequest[oidc.De } func (s *LegacyServer) authenticateResourceClient(ctx context.Context, cc *ClientCredentials) (string, error) { + ctx, span := tracer.Start(ctx, "LegacyServer.authenticateResourceClient") + defer span.End() + if cc.ClientAssertion != "" { if jp, ok := s.provider.(ClientJWTProfile); ok { return ClientJWTAuth(ctx, oidc.ClientAssertionParams{ClientAssertion: cc.ClientAssertion}, jp) @@ -327,6 +369,9 @@ func (s *LegacyServer) authenticateResourceClient(ctx context.Context, cc *Clien } func (s *LegacyServer) Introspect(ctx context.Context, r *Request[IntrospectionRequest]) (*Response, error) { + ctx, span := tracer.Start(ctx, "LegacyServer.Introspect") + defer span.End() + clientID, err := s.authenticateResourceClient(ctx, r.Data.ClientCredentials) if err != nil { return nil, err @@ -345,6 +390,9 @@ func (s *LegacyServer) Introspect(ctx context.Context, r *Request[IntrospectionR } func (s *LegacyServer) UserInfo(ctx context.Context, r *Request[oidc.UserInfoRequest]) (*Response, error) { + ctx, span := tracer.Start(ctx, "LegacyServer.UserInfo") + defer span.End() + tokenID, subject, ok := getTokenIDAndSubject(ctx, s.provider, r.Data.AccessToken) if !ok { return nil, NewStatusError(oidc.ErrAccessDenied().WithDescription("access token invalid"), http.StatusUnauthorized) @@ -358,6 +406,9 @@ func (s *LegacyServer) UserInfo(ctx context.Context, r *Request[oidc.UserInfoReq } func (s *LegacyServer) Revocation(ctx context.Context, r *ClientRequest[oidc.RevocationRequest]) (*Response, error) { + ctx, span := tracer.Start(ctx, "LegacyServer.Revocation") + defer span.End() + var subject string doDecrypt := true if r.Data.TokenTypeHint != "access_token" { @@ -387,6 +438,9 @@ func (s *LegacyServer) Revocation(ctx context.Context, r *ClientRequest[oidc.Rev } func (s *LegacyServer) EndSession(ctx context.Context, r *Request[oidc.EndSessionRequest]) (*Redirect, error) { + ctx, span := tracer.Start(ctx, "LegacyServer.EndSession") + defer span.End() + session, err := ValidateEndSessionRequest(ctx, r.Data, s.provider) if err != nil { return nil, err diff --git a/pkg/op/session.go b/pkg/op/session.go index 6af7d7c..8ac530d 100644 --- a/pkg/op/session.go +++ b/pkg/op/session.go @@ -27,6 +27,10 @@ func endSessionHandler(ender SessionEnder) func(http.ResponseWriter, *http.Reque } func EndSession(w http.ResponseWriter, r *http.Request, ender SessionEnder) { + ctx, span := tracer.Start(r.Context(), "EndSession") + defer span.End() + r = r.WithContext(ctx) + req, err := ParseEndSessionRequest(r, ender.Decoder()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -64,6 +68,9 @@ func ParseEndSessionRequest(r *http.Request, decoder httphelper.Decoder) (*oidc. } func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest, ender SessionEnder) (*EndSessionRequest, error) { + ctx, span := tracer.Start(ctx, "ValidateEndSessionRequest") + defer span.End() + session := &EndSessionRequest{ RedirectURI: ender.DefaultLogoutRedirectURI(), } diff --git a/pkg/op/token.go b/pkg/op/token.go index 83889f0..19edcce 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -69,6 +69,9 @@ func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Cli } func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storage, refreshToken string, client AccessTokenClient) (id, newRefreshToken string, exp time.Time, err error) { + ctx, span := tracer.Start(ctx, "createTokens") + defer span.End() + if needsRefreshToken(tokenRequest, client) { return storage.CreateAccessAndRefreshTokens(ctx, tokenRequest, refreshToken) } diff --git a/pkg/op/token_exchange.go b/pkg/op/token_exchange.go index db3e468..fcb4468 100644 --- a/pkg/op/token_exchange.go +++ b/pkg/op/token_exchange.go @@ -193,6 +193,9 @@ func ValidateTokenExchangeRequest( clientID, clientSecret string, exchanger Exchanger, ) (TokenExchangeRequest, Client, error) { + ctx, span := tracer.Start(ctx, "ValidateTokenExchangeRequest") + defer span.End() + if oidcTokenExchangeRequest.SubjectToken == "" { return nil, nil, oidc.ErrInvalidRequest().WithDescription("subject_token missing") } @@ -231,6 +234,9 @@ func CreateTokenExchangeRequest( client Client, exchanger Exchanger, ) (TokenExchangeRequest, error) { + ctx, span := tracer.Start(ctx, "CreateTokenExchangeRequest") + defer span.End() + teStorage, ok := exchanger.Storage().(TokenExchangeStorage) if !ok { return nil, unimplementedGrantError(oidc.GrantTypeTokenExchange) @@ -294,6 +300,9 @@ func GetTokenIDAndSubjectFromToken( tokenType oidc.TokenType, isActor bool, ) (tokenIDOrToken, subject string, claims map[string]any, ok bool) { + ctx, span := tracer.Start(ctx, "GetTokenIDAndSubjectFromToken") + defer span.End() + switch tokenType { case oidc.AccessTokenType: var accessTokenClaims *oidc.AccessTokenClaims @@ -341,6 +350,9 @@ func GetTokenIDAndSubjectFromToken( // AuthorizeTokenExchangeClient authorizes a client by validating the client_id and client_secret func AuthorizeTokenExchangeClient(ctx context.Context, clientID, clientSecret string, exchanger Exchanger) (client Client, err error) { + ctx, span := tracer.Start(ctx, "AuthorizeTokenExchangeClient") + defer span.End() + if err := AuthorizeClientIDSecret(ctx, clientID, clientSecret, exchanger.Storage()); err != nil { return nil, err } @@ -359,6 +371,8 @@ func CreateTokenExchangeResponse( client Client, creator TokenCreator, ) (_ *oidc.TokenExchangeResponse, err error) { + ctx, span := tracer.Start(ctx, "CreateTokenExchangeResponse") + defer span.End() var ( token, refreshToken, tokenType string diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go index 9c45ef8..29234e1 100644 --- a/pkg/op/token_intospection.go +++ b/pkg/op/token_intospection.go @@ -28,6 +28,10 @@ func introspectionHandler(introspector Introspector) func(http.ResponseWriter, * } func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspector) { + ctx, span := tracer.Start(r.Context(), "Introspect") + defer span.End() + r = r.WithContext(ctx) + response := new(oidc.IntrospectionResponse) token, clientID, err := ParseTokenIntrospectionRequest(r, introspector) if err != nil { diff --git a/pkg/op/token_refresh.go b/pkg/op/token_refresh.go index afca3bf..92ef476 100644 --- a/pkg/op/token_refresh.go +++ b/pkg/op/token_refresh.go @@ -141,6 +141,9 @@ func AuthorizeRefreshClient(ctx context.Context, tokenReq *oidc.RefreshTokenRequ // RefreshTokenRequestByRefreshToken returns the RefreshTokenRequest (data representing the original auth request) // corresponding to the refresh_token from Storage or an error func RefreshTokenRequestByRefreshToken(ctx context.Context, storage Storage, refreshToken string) (RefreshTokenRequest, error) { + ctx, span := tracer.Start(ctx, "RefreshTokenRequestByRefreshToken") + defer span.End() + request, err := storage.TokenRequestByRefreshToken(ctx, refreshToken) if err != nil { return nil, oidc.ErrInvalidGrant().WithParent(err) diff --git a/pkg/op/token_request.go b/pkg/op/token_request.go index 2006725..85e2270 100644 --- a/pkg/op/token_request.go +++ b/pkg/op/token_request.go @@ -37,6 +37,10 @@ func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Reque // Exchange performs a token exchange appropriate for the grant type func Exchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { + ctx, span := tracer.Start(r.Context(), "Exchange") + r = r.WithContext(ctx) + defer span.End() + grantType := r.FormValue("grant_type") switch grantType { case string(oidc.GrantTypeCode): @@ -115,6 +119,9 @@ func ParseAuthenticatedTokenRequest(r *http.Request, decoder httphelper.Decoder, // AuthorizeClientIDSecret authorizes a client by validating the client_id and client_secret (Basic Auth and POST) func AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string, storage Storage) error { + ctx, span := tracer.Start(ctx, "AuthorizeClientIDSecret") + defer span.End() + err := storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret) if err != nil { return oidc.ErrInvalidClient().WithDescription("invalid client_id / client_secret").WithParent(err) diff --git a/pkg/op/token_revocation.go b/pkg/op/token_revocation.go index d19c7f7..537b164 100644 --- a/pkg/op/token_revocation.go +++ b/pkg/op/token_revocation.go @@ -32,6 +32,10 @@ func revocationHandler(revoker Revoker) func(http.ResponseWriter, *http.Request) } func Revoke(w http.ResponseWriter, r *http.Request, revoker Revoker) { + ctx, span := tracer.Start(r.Context(), "Revoke") + r = r.WithContext(ctx) + defer span.End() + token, tokenTypeHint, clientID, err := ParseTokenRevocationRequest(r, revoker) if err != nil { RevocationRequestError(w, r, err) @@ -68,6 +72,10 @@ func Revoke(w http.ResponseWriter, r *http.Request, revoker Revoker) { } func ParseTokenRevocationRequest(r *http.Request, revoker Revoker) (token, tokenTypeHint, clientID string, err error) { + ctx, span := tracer.Start(r.Context(), "ParseTokenRevocationRequest") + r = r.WithContext(ctx) + defer span.End() + err = r.ParseForm() if err != nil { return "", "", "", oidc.ErrInvalidRequest().WithDescription("unable to parse request").WithParent(err) @@ -148,6 +156,9 @@ func RevocationError(err error) StatusError { } func getTokenIDAndSubjectForRevocation(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, bool) { + ctx, span := tracer.Start(ctx, "getTokenIDAndSubjectFromRevocation") + defer span.End() + tokenIDSubject, err := userinfoProvider.Crypto().Decrypt(accessToken) if err == nil { splitToken := strings.Split(tokenIDSubject, ":") diff --git a/pkg/op/userinfo.go b/pkg/op/userinfo.go index 86205b5..4b8f398 100644 --- a/pkg/op/userinfo.go +++ b/pkg/op/userinfo.go @@ -24,6 +24,10 @@ func userinfoHandler(userinfoProvider UserinfoProvider) func(http.ResponseWriter } func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoProvider) { + ctx, span := tracer.Start(r.Context(), "Userinfo") + r = r.WithContext(ctx) + defer span.End() + accessToken, err := ParseUserinfoRequest(r, userinfoProvider.Decoder()) if err != nil { http.Error(w, "access token missing", http.StatusUnauthorized) @@ -44,6 +48,10 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP } func ParseUserinfoRequest(r *http.Request, decoder httphelper.Decoder) (string, error) { + ctx, span := tracer.Start(r.Context(), "ParseUserinfoRequest") + r = r.WithContext(ctx) + defer span.End() + accessToken, err := getAccessToken(r) if err == nil { return accessToken, nil @@ -61,6 +69,10 @@ func ParseUserinfoRequest(r *http.Request, decoder httphelper.Decoder) (string, } func getAccessToken(r *http.Request) (string, error) { + ctx, span := tracer.Start(r.Context(), "RefreshTokens") + r = r.WithContext(ctx) + defer span.End() + authHeader := r.Header.Get("authorization") if authHeader == "" { return "", errors.New("no auth header") @@ -73,6 +85,9 @@ func getAccessToken(r *http.Request) (string, error) { } func getTokenIDAndSubject(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, bool) { + ctx, span := tracer.Start(ctx, "getTokenIDAndSubject") + defer span.End() + tokenIDSubject, err := userinfoProvider.Crypto().Decrypt(accessToken) if err == nil { splitToken := strings.Split(tokenIDSubject, ":") diff --git a/pkg/op/verifier_access_token.go b/pkg/op/verifier_access_token.go index 120bfa7..6ac29f2 100644 --- a/pkg/op/verifier_access_token.go +++ b/pkg/op/verifier_access_token.go @@ -30,6 +30,9 @@ func NewAccessTokenVerifier(issuer string, keySet oidc.KeySet, opts ...AccessTok // VerifyAccessToken validates the access token (issuer, signature and expiration). func VerifyAccessToken[C oidc.Claims](ctx context.Context, token string, v *AccessTokenVerifier) (claims C, err error) { + ctx, span := tracer.Start(ctx, "VerifyAccessToken") + defer span.End() + var nilClaims C decrypted, err := oidc.DecryptToken(token) diff --git a/pkg/op/verifier_id_token_hint.go b/pkg/op/verifier_id_token_hint.go index b5ec72e..331c64c 100644 --- a/pkg/op/verifier_id_token_hint.go +++ b/pkg/op/verifier_id_token_hint.go @@ -46,6 +46,9 @@ func (e IDTokenHintExpiredError) Is(err error) bool { // is returned of type [IDTokenHintExpiredError]. In that case the caller can choose to still // trust the token for cases like logout, as signature and other verifications succeeded. func VerifyIDTokenHint[C oidc.Claims](ctx context.Context, token string, v *IDTokenHintVerifier) (claims C, err error) { + ctx, span := tracer.Start(ctx, "VerifyIDTokenHint") + defer span.End() + var nilClaims C decrypted, err := oidc.DecryptToken(token) diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index 38b8ee4..ced99ad 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -118,6 +118,9 @@ type jwtProfileKeySet struct { // VerifySignature implements oidc.KeySet by getting the public key from Storage implementation func (k *jwtProfileKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) { + ctx, span := tracer.Start(ctx, "VerifySignature") + defer span.End() + keyID, _ := oidc.GetKeyIDAndAlg(jws) key, err := k.storage.GetKeyByIDAndClientID(ctx, keyID, k.clientID) if err != nil { From bdcccc3303a96cdf5d8e410abc77f2a09c64e1a1 Mon Sep 17 00:00:00 2001 From: adlerhurst Date: Wed, 6 Mar 2024 18:39:27 +0100 Subject: [PATCH 363/502] feat(client): tracing in rp --- pkg/client/rp/device.go | 6 ++++++ pkg/client/rp/jwks.go | 15 ++++++++++++++ pkg/client/rp/relying_party.go | 37 ++++++++++++++++++++++++++++++++++ pkg/client/rp/verifier.go | 6 ++++++ pkg/op/op.go | 1 + 5 files changed, 65 insertions(+) diff --git a/pkg/client/rp/device.go b/pkg/client/rp/device.go index 02c647e..ec4dabe 100644 --- a/pkg/client/rp/device.go +++ b/pkg/client/rp/device.go @@ -33,6 +33,9 @@ func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc. // in RFC 8628, section 3.1 and 3.2: // https://www.rfc-editor.org/rfc/rfc8628#section-3.1 func DeviceAuthorization(ctx context.Context, scopes []string, rp RelyingParty, authFn any) (*oidc.DeviceAuthorizationResponse, error) { + ctx, span := tracer.Start(ctx, "DeviceAuthorization") + defer span.End() + ctx = logCtxWithRPData(ctx, rp, "function", "DeviceAuthorization") req, err := newDeviceClientCredentialsRequest(scopes, rp) if err != nil { @@ -46,6 +49,9 @@ func DeviceAuthorization(ctx context.Context, scopes []string, rp RelyingParty, // by means of polling as defined in RFC, section 3.3 and 3.4: // https://www.rfc-editor.org/rfc/rfc8628#section-3.4 func DeviceAccessToken(ctx context.Context, deviceCode string, interval time.Duration, rp RelyingParty) (resp *oidc.AccessTokenResponse, err error) { + ctx, span := tracer.Start(ctx, "DeviceAccessToken") + defer span.End() + ctx = logCtxWithRPData(ctx, rp, "function", "DeviceAccessToken") req := &client.DeviceAccessTokenRequest{ DeviceAccessTokenRequest: oidc.DeviceAccessTokenRequest{ diff --git a/pkg/client/rp/jwks.go b/pkg/client/rp/jwks.go index 28aec9b..a3c1647 100644 --- a/pkg/client/rp/jwks.go +++ b/pkg/client/rp/jwks.go @@ -83,6 +83,9 @@ func (i *inflight) result() ([]jose.JSONWebKey, error) { } func (r *remoteKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { + ctx, span := tracer.Start(ctx, "VerifySignature") + defer span.End() + keyID, alg := oidc.GetKeyIDAndAlg(jws) if alg == "" { alg = r.defaultAlg @@ -135,6 +138,9 @@ func (r *remoteKeySet) exactMatch(jwkID, jwsID string) bool { } func (r *remoteKeySet) verifySignatureRemote(ctx context.Context, jws *jose.JSONWebSignature, keyID, alg string) ([]byte, error) { + ctx, span := tracer.Start(ctx, "verifySignatureRemote") + defer span.End() + keys, err := r.keysFromRemote(ctx) if err != nil { return nil, fmt.Errorf("unable to fetch key for signature validation: %w", err) @@ -159,6 +165,9 @@ func (r *remoteKeySet) keysFromCache() (keys []jose.JSONWebKey) { // keysFromRemote syncs the key set from the remote set, records the values in the // cache, and returns the key set. func (r *remoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, error) { + ctx, span := tracer.Start(ctx, "keysFromRemote") + defer span.End() + // Need to lock to inspect the inflight request field. r.mu.Lock() // If there's not a current inflight request, create one. @@ -182,6 +191,9 @@ func (r *remoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, e } func (r *remoteKeySet) updateKeys(ctx context.Context) { + ctx, span := tracer.Start(ctx, "updateKeys") + defer span.End() + // Sync keys and finish inflight when that's done. keys, err := r.fetchRemoteKeys(ctx) @@ -201,6 +213,9 @@ func (r *remoteKeySet) updateKeys(ctx context.Context) { } func (r *remoteKeySet) fetchRemoteKeys(ctx context.Context) ([]jose.JSONWebKey, error) { + ctx, span := tracer.Start(ctx, "fetchRemoteKeys") + defer span.End() + req, err := http.NewRequest("GET", r.jwksURL, nil) if err != nil { return nil, fmt.Errorf("oidc: can't create request: %v", err) diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 74da71e..3022035 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -12,6 +12,8 @@ import ( "github.com/go-jose/go-jose/v3" "github.com/google/uuid" "github.com/zitadel/logging" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" @@ -28,6 +30,12 @@ const ( var ErrUserInfoSubNotMatching = errors.New("sub from userinfo does not match the sub from the id_token") +var tracer trace.Tracer + +func init() { + tracer = otel.Tracer("github.com/zitadel/oidc/pkg/client/rp") +} + // RelyingParty declares the minimal interface for oidc clients type RelyingParty interface { // OAuthConfig returns the oauth2 Config @@ -428,6 +436,9 @@ func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (stri var ErrMissingIDToken = errors.New("id_token missing") func verifyTokenResponse[C oidc.IDClaims](ctx context.Context, token *oauth2.Token, rp RelyingParty) (*oidc.Tokens[C], error) { + ctx, span := tracer.Start(ctx, "verifyTokenResponse") + defer span.End() + if rp.IsOAuth2Only() { return &oidc.Tokens[C]{Token: token}, nil } @@ -445,6 +456,9 @@ func verifyTokenResponse[C oidc.IDClaims](ctx context.Context, token *oauth2.Tok // CodeExchange handles the oauth2 code exchange, extracting and validating the id_token // returning it parsed together with the oauth2 tokens (access, refresh) func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens[C], err error) { + ctx, codeExchangeSpan := tracer.Start(ctx, "CodeExchange") + defer codeExchangeSpan.End() + ctx = logCtxWithRPData(ctx, rp, "function", "CodeExchange") ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient()) codeOpts := make([]oauth2.AuthCodeOption, 0) @@ -452,10 +466,12 @@ func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingP codeOpts = append(codeOpts, opt()...) } + ctx, oauthExchangeSpan := tracer.Start(ctx, "OAuthExchange") token, err := rp.OAuthConfig().Exchange(ctx, code, codeOpts...) if err != nil { return nil, err } + oauthExchangeSpan.End() return verifyTokenResponse[C](ctx, token, rp) } @@ -469,6 +485,9 @@ func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingP // [RFC 6749, section 4.4]: https://datatracker.ietf.org/doc/html/rfc6749#section-4.4 func ClientCredentials(ctx context.Context, rp RelyingParty, endpointParams url.Values) (token *oauth2.Token, err error) { ctx = logCtxWithRPData(ctx, rp, "function", "ClientCredentials") + ctx, span := tracer.Start(ctx, "ClientCredentials") + defer span.End() + ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient()) config := clientcredentials.Config{ ClientID: rp.OAuthConfig().ClientID, @@ -489,6 +508,10 @@ type CodeExchangeCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.R // Custom parameters can optionally be set to the token URL. func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "CodeExchangeHandler") + r = r.WithContext(ctx) + defer span.End() + state, err := tryReadStateCookie(w, r, rp) if err != nil { unauthorizedError(w, r, "failed to get state: "+err.Error(), state, rp) @@ -540,6 +563,10 @@ type CodeExchangeUserinfoCallback[C oidc.IDClaims, U SubjectGetter] func(w http. // on success it will pass the userinfo into its callback function as well func UserinfoCallback[C oidc.IDClaims, U SubjectGetter](f CodeExchangeUserinfoCallback[C, U]) CodeExchangeCallback[C] { return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) { + ctx, span := tracer.Start(r.Context(), "UserinfoCallback") + r = r.WithContext(ctx) + defer span.End() + info, err := Userinfo[U](r.Context(), tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp) if err != nil { unauthorizedError(w, r, "userinfo failed: "+err.Error(), state, rp) @@ -558,6 +585,8 @@ func UserinfoCallback[C oidc.IDClaims, U SubjectGetter](f CodeExchangeUserinfoCa func Userinfo[U SubjectGetter](ctx context.Context, token, tokenType, subject string, rp RelyingParty) (userinfo U, err error) { var nilU U ctx = logCtxWithRPData(ctx, rp, "function", "Userinfo") + ctx, span := tracer.Start(ctx, "Userinfo") + defer span.End() req, err := http.NewRequestWithContext(ctx, http.MethodGet, rp.UserinfoEndpoint(), nil) if err != nil { @@ -716,6 +745,9 @@ type RefreshTokenRequest struct { // the IDToken and AccessToken will be verfied // and the IDToken and IDTokenClaims fields will be populated in the returned object. func RefreshTokens[C oidc.IDClaims](ctx context.Context, rp RelyingParty, refreshToken, clientAssertion, clientAssertionType string) (*oidc.Tokens[C], error) { + ctx, span := tracer.Start(ctx, "RefreshTokens") + defer span.End() + ctx = logCtxWithRPData(ctx, rp, "function", "RefreshTokens") request := RefreshTokenRequest{ RefreshToken: refreshToken, @@ -741,6 +773,9 @@ func RefreshTokens[C oidc.IDClaims](ctx context.Context, rp RelyingParty, refres func EndSession(ctx context.Context, rp RelyingParty, idToken, optionalRedirectURI, optionalState string) (*url.URL, error) { ctx = logCtxWithRPData(ctx, rp, "function", "EndSession") + ctx, span := tracer.Start(ctx, "RefreshTokens") + defer span.End() + request := oidc.EndSessionRequest{ IdTokenHint: idToken, ClientID: rp.OAuthConfig().ClientID, @@ -757,6 +792,8 @@ func EndSession(ctx context.Context, rp RelyingParty, idToken, optionalRedirectU // tokenTypeHint should be either "id_token" or "refresh_token". func RevokeToken(ctx context.Context, rp RelyingParty, token string, tokenTypeHint string) error { ctx = logCtxWithRPData(ctx, rp, "function", "RevokeToken") + ctx, span := tracer.Start(ctx, "RefreshTokens") + defer span.End() request := client.RevokeRequest{ Token: token, TokenTypeHint: tokenTypeHint, diff --git a/pkg/client/rp/verifier.go b/pkg/client/rp/verifier.go index adf8872..ebc10cc 100644 --- a/pkg/client/rp/verifier.go +++ b/pkg/client/rp/verifier.go @@ -12,6 +12,9 @@ import ( // VerifyTokens implement the Token Response Validation as defined in OIDC specification // https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken string, v *IDTokenVerifier) (claims C, err error) { + ctx, span := tracer.Start(ctx, "VerifyTokens") + defer span.End() + var nilClaims C claims, err = VerifyIDToken[C](ctx, idToken, v) @@ -27,6 +30,9 @@ func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken str // VerifyIDToken validates the id token according to // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v *IDTokenVerifier) (claims C, err error) { + ctx, span := tracer.Start(ctx, "VerifyIDToken") + defer span.End() + var nilClaims C decrypted, err := oidc.DecryptToken(token) diff --git a/pkg/op/op.go b/pkg/op/op.go index 326737a..2080339 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -135,6 +135,7 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) chi.Router } else { router.Use(cors.New(defaultCORSOptions).Handler) } + router.Use(intercept(o.IssuerFromRequest, interceptors...)) router.HandleFunc(healthEndpoint, healthHandler) router.HandleFunc(readinessEndpoint, readyHandler(o.Probes())) From 88209ac11d0d105bebb5eff9284db80b577c3e32 Mon Sep 17 00:00:00 2001 From: adlerhurst Date: Wed, 6 Mar 2024 19:08:48 +0100 Subject: [PATCH 364/502] fix tests --- pkg/op/auth_request_test.go | 6 +++--- pkg/op/client_test.go | 6 +++--- pkg/op/op_test.go | 2 +- pkg/op/server_http_routes_test.go | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index 76cb00d..45627a5 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -1072,7 +1072,7 @@ func TestAuthResponseCode(t *testing.T) { authorizer: func(t *testing.T) op.Authorizer { ctrl := gomock.NewController(t) storage := mock.NewMockStorage(ctrl) - storage.EXPECT().SaveAuthCode(context.Background(), "id1", "id1") + storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1") authorizer := mock.NewMockAuthorizer(ctrl) authorizer.EXPECT().Storage().Return(storage) @@ -1097,7 +1097,7 @@ func TestAuthResponseCode(t *testing.T) { authorizer: func(t *testing.T) op.Authorizer { ctrl := gomock.NewController(t) storage := mock.NewMockStorage(ctrl) - storage.EXPECT().SaveAuthCode(context.Background(), "id1", "id1") + storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1") authorizer := mock.NewMockAuthorizer(ctrl) authorizer.EXPECT().Storage().Return(storage) @@ -1124,7 +1124,7 @@ func TestAuthResponseCode(t *testing.T) { authorizer: func(t *testing.T) op.Authorizer { ctrl := gomock.NewController(t) storage := mock.NewMockStorage(ctrl) - storage.EXPECT().SaveAuthCode(context.Background(), "id1", "id1") + storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1") authorizer := mock.NewMockAuthorizer(ctrl) authorizer.EXPECT().Storage().Return(storage) diff --git a/pkg/op/client_test.go b/pkg/op/client_test.go index 0321f88..b772ba5 100644 --- a/pkg/op/client_test.go +++ b/pkg/op/client_test.go @@ -108,7 +108,7 @@ func TestClientBasicAuth(t *testing.T) { }, storage: func() op.Storage { s := mock.NewMockStorage(gomock.NewController(t)) - s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "wrong").Return(errWrong) + s.EXPECT().AuthorizeClientIDSecret(gomock.Any(), "foo", "wrong").Return(errWrong) return s }(), wantErr: errWrong, @@ -121,7 +121,7 @@ func TestClientBasicAuth(t *testing.T) { }, storage: func() op.Storage { s := mock.NewMockStorage(gomock.NewController(t)) - s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "bar").Return(nil) + s.EXPECT().AuthorizeClientIDSecret(gomock.Any(), "foo", "bar").Return(nil) return s }(), wantClientID: "foo", @@ -207,7 +207,7 @@ func TestClientIDFromRequest(t *testing.T) { p: testClientProvider{ storage: func() op.Storage { s := mock.NewMockStorage(gomock.NewController(t)) - s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "bar").Return(nil) + s.EXPECT().AuthorizeClientIDSecret(gomock.Any(), "foo", "bar").Return(nil) return s }(), }, diff --git a/pkg/op/op_test.go b/pkg/op/op_test.go index b2a758c..5e0d675 100644 --- a/pkg/op/op_test.go +++ b/pkg/op/op_test.go @@ -235,7 +235,7 @@ func TestRoutes(t *testing.T) { contains: []string{`{"access_token":"`, `","token_type":"Bearer","expires_in":299}`}, }, { - // This call will fail. A successfull test is already + // This call will fail. A successful test is already // part of device_test.go name: "device token", method: http.MethodPost, diff --git a/pkg/op/server_http_routes_test.go b/pkg/op/server_http_routes_test.go index c50e989..8b3fa02 100644 --- a/pkg/op/server_http_routes_test.go +++ b/pkg/op/server_http_routes_test.go @@ -177,7 +177,7 @@ func TestServerRoutes(t *testing.T) { contains: []string{`{"access_token":"`, `","token_type":"Bearer","expires_in":299}`}, }, { - // This call will fail. A successfull test is already + // This call will fail. A successful test is already // part of device_test.go name: "device token", method: http.MethodPost, From 7069813ec71e47a5ad7c2cc8c16a708946a83129 Mon Sep 17 00:00:00 2001 From: adlerhurst Date: Thu, 7 Mar 2024 10:44:24 +0100 Subject: [PATCH 365/502] correct span names --- pkg/op/auth_request.go | 3 +-- pkg/op/device.go | 2 +- pkg/op/op.go | 1 - pkg/op/token_revocation.go | 2 +- pkg/op/userinfo.go | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 435e3f4..4b5837a 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -79,7 +79,6 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { AuthRequestError(w, r, nil, err, authorizer) return } - ctx = r.Context() if authReq.RequestParam != "" && authorizer.RequestObjectSupported() { err = ParseRequestObject(ctx, authReq, authorizer.Storage(), IssuerFromContext(ctx)) if err != nil { @@ -452,7 +451,7 @@ func ParseAuthorizeCallbackRequest(r *http.Request) (id string, err error) { // AuthResponse creates the successful authentication response (either code or tokens) func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "ValidateAuthRequest") + ctx, span := tracer.Start(r.Context(), "AuthResponse") r = r.WithContext(ctx) defer span.End() diff --git a/pkg/op/device.go b/pkg/op/device.go index d08b4fd..11638b0 100644 --- a/pkg/op/device.go +++ b/pkg/op/device.go @@ -299,7 +299,7 @@ func (r *DeviceAuthorizationState) GetSubject() string { } func CheckDeviceAuthorizationState(ctx context.Context, clientID, deviceCode string, exchanger Exchanger) (*DeviceAuthorizationState, error) { - ctx, span := tracer.Start(ctx, "CheckDeviceAuthorization") + ctx, span := tracer.Start(ctx, "CheckDeviceAuthorizationState") defer span.End() storage, err := assertDeviceStorage(exchanger.Storage()) diff --git a/pkg/op/op.go b/pkg/op/op.go index 2080339..326737a 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -135,7 +135,6 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) chi.Router } else { router.Use(cors.New(defaultCORSOptions).Handler) } - router.Use(intercept(o.IssuerFromRequest, interceptors...)) router.HandleFunc(healthEndpoint, healthHandler) router.HandleFunc(readinessEndpoint, readyHandler(o.Probes())) diff --git a/pkg/op/token_revocation.go b/pkg/op/token_revocation.go index 537b164..a86a481 100644 --- a/pkg/op/token_revocation.go +++ b/pkg/op/token_revocation.go @@ -156,7 +156,7 @@ func RevocationError(err error) StatusError { } func getTokenIDAndSubjectForRevocation(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, bool) { - ctx, span := tracer.Start(ctx, "getTokenIDAndSubjectFromRevocation") + ctx, span := tracer.Start(ctx, "getTokenIDAndSubjectForRevocation") defer span.End() tokenIDSubject, err := userinfoProvider.Crypto().Decrypt(accessToken) diff --git a/pkg/op/userinfo.go b/pkg/op/userinfo.go index 4b8f398..839b139 100644 --- a/pkg/op/userinfo.go +++ b/pkg/op/userinfo.go @@ -69,7 +69,7 @@ func ParseUserinfoRequest(r *http.Request, decoder httphelper.Decoder) (string, } func getAccessToken(r *http.Request) (string, error) { - ctx, span := tracer.Start(r.Context(), "RefreshTokens") + ctx, span := tracer.Start(r.Context(), "getAccessToken") r = r.WithContext(ctx) defer span.End() From 0fe7c3307f1bb3773b24d54c9ae052420248f58c Mon Sep 17 00:00:00 2001 From: adlerhurst Date: Thu, 7 Mar 2024 15:25:23 +0100 Subject: [PATCH 366/502] fix parse --- pkg/op/client.go | 8 ++++---- pkg/op/op_test.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/op/client.go b/pkg/op/client.go index e0a0443..913944c 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -153,15 +153,15 @@ type clientData struct { // If no client id can be obtained by any method, oidc.ErrInvalidClient // is returned with ErrMissingClientID wrapped in it. func ClientIDFromRequest(r *http.Request, p ClientProvider) (clientID string, authenticated bool, err error) { - ctx, span := tracer.Start(r.Context(), "ClientIDFromRequest") - r = r.WithContext(ctx) - defer span.End() - err = r.ParseForm() if err != nil { return "", false, oidc.ErrInvalidRequest().WithDescription("cannot parse form").WithParent(err) } + ctx, span := tracer.Start(r.Context(), "ClientIDFromRequest") + r = r.WithContext(ctx) + defer span.End() + data := new(clientData) if err = p.Decoder().Decode(data, r.Form); err != nil { return "", false, err diff --git a/pkg/op/op_test.go b/pkg/op/op_test.go index 5e0d675..83032d4 100644 --- a/pkg/op/op_test.go +++ b/pkg/op/op_test.go @@ -181,7 +181,7 @@ func TestRoutes(t *testing.T) { }, }, { - // This call will fail. A successfull test is already + // This call will fail. A successful test is already // part of client/integration_test.go name: "code exchange", method: http.MethodGet, From 1532a5c78b8418d91635038e0df4d014813811a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Mar 2024 07:35:47 +0100 Subject: [PATCH 367/502] chore(deps): bump github.com/go-jose/go-jose/v3 from 3.0.2 to 3.0.3 (#566) Bumps [github.com/go-jose/go-jose/v3](https://github.com/go-jose/go-jose) from 3.0.2 to 3.0.3. - [Release notes](https://github.com/go-jose/go-jose/releases) - [Changelog](https://github.com/go-jose/go-jose/blob/v3.0.3/CHANGELOG.md) - [Commits](https://github.com/go-jose/go-jose/compare/v3.0.2...v3.0.3) --- updated-dependencies: - dependency-name: github.com/go-jose/go-jose/v3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9cb99e6..c7562ea 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 require ( github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/go-chi/chi/v5 v5.0.12 - github.com/go-jose/go-jose/v3 v3.0.2 + github.com/go-jose/go-jose/v3 v3.0.3 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index 9b2c86f..2b1eb7e 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ 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/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-jose/go-jose/v3 v3.0.2 h1:2Edjn8Nrb44UvTdp84KU0bBPs1cO7noRCybtS3eJEUQ= -github.com/go-jose/go-jose/v3 v3.0.2/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= +github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= From ad798029685818564a91afdad5d114031563ae99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 13 Mar 2024 16:26:09 +0200 Subject: [PATCH 368/502] feat: extend token exchange response (#567) * feat: extend token exchange response This change adds fields to the token exchange and token claims types. The `act` claim has been added to describe the actor in case of impersonation or delegation. An actor can be nested in case an obtained token is used as actor token to obtain impersonation or delegation. This allows creating a chain of actors. See [RFC 8693, section 4.1](https://www.rfc-editor.org/rfc/rfc8693#name-act-actor-claim). The `id_token` field has been added to the Token Exchange response so an ID Token can be returned along with an access token. This is not specified in RFC 8693, but it allows us be consistent with OpenID responses when the scope `openid` is set, while the requested token type may remain access token. * allow jwt profile for token exchange client * add invalid target error --- pkg/client/tokenexchange/tokenexchange.go | 13 ++++++ pkg/oidc/error.go | 13 ++++++ pkg/oidc/token.go | 53 +++++++++++++++++------ pkg/oidc/token_request.go | 9 +--- 4 files changed, 68 insertions(+), 20 deletions(-) diff --git a/pkg/client/tokenexchange/tokenexchange.go b/pkg/client/tokenexchange/tokenexchange.go index fdac833..7bc35a2 100644 --- a/pkg/client/tokenexchange/tokenexchange.go +++ b/pkg/client/tokenexchange/tokenexchange.go @@ -4,7 +4,9 @@ import ( "context" "errors" "net/http" + "time" + "github.com/go-jose/go-jose/v3" "github.com/zitadel/oidc/v3/pkg/client" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" @@ -33,6 +35,17 @@ func NewTokenExchangerClientCredentials(ctx context.Context, issuer, clientID, c return newOAuthTokenExchange(ctx, issuer, authorizer, options...) } +func NewTokenExchangerJWTProfile(ctx context.Context, issuer, clientID string, signer jose.Signer, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) { + authorizer := func() (any, error) { + assertion, err := client.SignedJWTProfileAssertion(clientID, []string{issuer}, time.Hour, signer) + if err != nil { + return nil, err + } + return client.ClientAssertionFormAuthorization(assertion), nil + } + return newOAuthTokenExchange(ctx, issuer, authorizer, options...) +} + func newOAuthTokenExchange(ctx context.Context, issuer string, authorizer func() (any, error), options ...func(source *OAuthTokenExchange)) (*OAuthTokenExchange, error) { te := &OAuthTokenExchange{ httpClient: httphelper.DefaultHTTPClient, diff --git a/pkg/oidc/error.go b/pkg/oidc/error.go index 86f8724..2f0572d 100644 --- a/pkg/oidc/error.go +++ b/pkg/oidc/error.go @@ -27,6 +27,11 @@ const ( SlowDown errorType = "slow_down" AccessDenied errorType = "access_denied" ExpiredToken errorType = "expired_token" + + // InvalidTarget error is returned by Token Exchange if + // the requested target or audience is invalid. + // [RFC 8693, Section 2.2.2: Error Response](https://www.rfc-editor.org/rfc/rfc8693#section-2.2.2) + InvalidTarget errorType = "invalid_target" ) var ( @@ -112,6 +117,14 @@ var ( Description: "The \"device_code\" has expired.", } } + + // Token exchange error + ErrInvalidTarget = func() *Error { + return &Error{ + ErrorType: InvalidTarget, + Description: "The requested audience or target is invalid.", + } + } ) type Error struct { diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index b4cb6b6..73eb2e5 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -34,19 +34,20 @@ type Tokens[C IDClaims] struct { // TokenClaims implements the Claims interface, // and can be used to extend larger claim types by embedding. type TokenClaims struct { - Issuer string `json:"iss,omitempty"` - Subject string `json:"sub,omitempty"` - Audience Audience `json:"aud,omitempty"` - Expiration Time `json:"exp,omitempty"` - IssuedAt Time `json:"iat,omitempty"` - AuthTime Time `json:"auth_time,omitempty"` - NotBefore Time `json:"nbf,omitempty"` - Nonce string `json:"nonce,omitempty"` - AuthenticationContextClassReference string `json:"acr,omitempty"` - AuthenticationMethodsReferences []string `json:"amr,omitempty"` - AuthorizedParty string `json:"azp,omitempty"` - ClientID string `json:"client_id,omitempty"` - JWTID string `json:"jti,omitempty"` + Issuer string `json:"iss,omitempty"` + Subject string `json:"sub,omitempty"` + Audience Audience `json:"aud,omitempty"` + Expiration Time `json:"exp,omitempty"` + IssuedAt Time `json:"iat,omitempty"` + AuthTime Time `json:"auth_time,omitempty"` + NotBefore Time `json:"nbf,omitempty"` + Nonce string `json:"nonce,omitempty"` + AuthenticationContextClassReference string `json:"acr,omitempty"` + AuthenticationMethodsReferences []string `json:"amr,omitempty"` + AuthorizedParty string `json:"azp,omitempty"` + ClientID string `json:"client_id,omitempty"` + JWTID string `json:"jti,omitempty"` + Actor *ActorClaims `json:"act,omitempty"` // Additional information set by this framework SignatureAlg jose.SignatureAlgorithm `json:"-"` @@ -204,6 +205,28 @@ func (i *IDTokenClaims) UnmarshalJSON(data []byte) error { return unmarshalJSONMulti(data, (*itcAlias)(i), &i.Claims) } +// ActorClaims provides the `act` claims used for impersonation or delegation Token Exchange. +// +// An actor can be nested in case an obtained token is used as actor token to obtain impersonation or delegation. +// This allows creating a chain of actors. +// See [RFC 8693, section 4.1](https://www.rfc-editor.org/rfc/rfc8693#name-act-actor-claim). +type ActorClaims struct { + Actor *ActorClaims `json:"act,omitempty"` + Issuer string `json:"iss,omitempty"` + Subject string `json:"sub,omitempty"` + Claims map[string]any `json:"-"` +} + +type acAlias ActorClaims + +func (c *ActorClaims) MarshalJSON() ([]byte, error) { + return mergeAndMarshalClaims((*acAlias)(c), c.Claims) +} + +func (c *ActorClaims) UnmarshalJSON(data []byte) error { + return unmarshalJSONMulti(data, (*acAlias)(c), &c.Claims) +} + type AccessTokenResponse struct { AccessToken string `json:"access_token,omitempty" schema:"access_token,omitempty"` TokenType string `json:"token_type,omitempty" schema:"token_type,omitempty"` @@ -352,4 +375,8 @@ type TokenExchangeResponse struct { ExpiresIn uint64 `json:"expires_in,omitempty"` Scopes SpaceDelimitedArray `json:"scope,omitempty"` RefreshToken string `json:"refresh_token,omitempty"` + + // IDToken field allows returning an additional ID token + // if the requested_token_type was Access Token and scope contained openid. + IDToken string `json:"id_token,omitempty"` } diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index b43b249..b07b333 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -3,6 +3,7 @@ package oidc import ( "encoding/json" "fmt" + "slices" "time" jose "github.com/go-jose/go-jose/v3" @@ -57,13 +58,7 @@ var AllTokenTypes = []TokenType{ type TokenType string func (t TokenType) IsSupported() bool { - for _, tt := range AllTokenTypes { - if t == tt { - return true - } - } - - return false + return slices.Contains(AllTokenTypes, t) } type TokenRequest interface { From 1b94f796eb8443f2f84c16d81b555b0df263cc40 Mon Sep 17 00:00:00 2001 From: adlerhurst Date: Wed, 13 Mar 2024 15:45:03 +0100 Subject: [PATCH 369/502] move tracer to client, add tracing in rs, client --- pkg/client/client.go | 33 ++++++++++++++++++++--- pkg/client/rp/device.go | 4 +-- pkg/client/rp/jwks.go | 11 ++++---- pkg/client/rp/relying_party.go | 30 ++++++++------------- pkg/client/rp/verifier.go | 5 ++-- pkg/client/rs/resource_server.go | 3 +++ pkg/client/tokenexchange/tokenexchange.go | 3 +++ pkg/op/op.go | 7 +---- 8 files changed, 59 insertions(+), 37 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index b329b3d..8b60264 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -12,19 +12,25 @@ import ( "time" jose "github.com/go-jose/go-jose/v3" - "golang.org/x/oauth2" - "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/crypto" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" + "go.opentelemetry.io/otel" + "golang.org/x/oauth2" ) -var Encoder = httphelper.Encoder(oidc.NewEncoder()) +var ( + Encoder = httphelper.Encoder(oidc.NewEncoder()) + Tracer = otel.Tracer("github.com/zitadel/oidc/pkg/client") +) // Discover calls the discovery endpoint of the provided issuer and returns its configuration // It accepts an optional argument "wellknownUrl" which can be used to overide the dicovery endpoint url func Discover(ctx context.Context, issuer string, httpClient *http.Client, wellKnownUrl ...string) (*oidc.DiscoveryConfiguration, error) { + ctx, span := Tracer.Start(ctx, "Discover") + defer span.End() + wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint if len(wellKnownUrl) == 1 && wellKnownUrl[0] != "" { wellKnown = wellKnownUrl[0] @@ -58,6 +64,9 @@ func CallTokenEndpoint(ctx context.Context, request any, caller TokenEndpointCal } func callTokenEndpoint(ctx context.Context, request any, authFn any, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) { + ctx, span := Tracer.Start(ctx, "callTokenEndpoint") + defer span.End() + req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, authFn) if err != nil { return nil, err @@ -86,6 +95,9 @@ type EndSessionCaller interface { } func CallEndSessionEndpoint(ctx context.Context, request any, authFn any, caller EndSessionCaller) (*url.URL, error) { + ctx, span := Tracer.Start(ctx, "CallEndSessionEndpoint") + defer span.End() + req, err := httphelper.FormRequest(ctx, caller.GetEndSessionEndpoint(), request, Encoder, authFn) if err != nil { return nil, err @@ -129,6 +141,9 @@ type RevokeRequest struct { } func CallRevokeEndpoint(ctx context.Context, request any, authFn any, caller RevokeCaller) error { + ctx, span := Tracer.Start(ctx, "CallRevokeEndpoint") + defer span.End() + req, err := httphelper.FormRequest(ctx, caller.GetRevokeEndpoint(), request, Encoder, authFn) if err != nil { return err @@ -157,6 +172,9 @@ func CallRevokeEndpoint(ctx context.Context, request any, authFn any, caller Rev } func CallTokenExchangeEndpoint(ctx context.Context, request any, authFn any, caller TokenEndpointCaller) (resp *oidc.TokenExchangeResponse, err error) { + ctx, span := Tracer.Start(ctx, "CallTokenExchangeEndpoint") + defer span.End() + req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, authFn) if err != nil { return nil, err @@ -198,6 +216,9 @@ type DeviceAuthorizationCaller interface { } func CallDeviceAuthorizationEndpoint(ctx context.Context, request *oidc.ClientCredentialsRequest, caller DeviceAuthorizationCaller, authFn any) (*oidc.DeviceAuthorizationResponse, error) { + ctx, span := Tracer.Start(ctx, "CallDeviceAuthorizationEndpoint") + defer span.End() + req, err := httphelper.FormRequest(ctx, caller.GetDeviceAuthorizationEndpoint(), request, Encoder, authFn) if err != nil { return nil, err @@ -219,6 +240,9 @@ type DeviceAccessTokenRequest struct { } func CallDeviceAccessTokenEndpoint(ctx context.Context, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) { + ctx, span := Tracer.Start(ctx, "CallDeviceAccessTokenEndpoint") + defer span.End() + req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, nil) if err != nil { return nil, err @@ -249,6 +273,9 @@ func CallDeviceAccessTokenEndpoint(ctx context.Context, request *DeviceAccessTok } func PollDeviceAccessTokenEndpoint(ctx context.Context, interval time.Duration, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) { + ctx, span := Tracer.Start(ctx, "PollDeviceAccessTokenEndpoint") + defer span.End() + for { timer := time.After(interval) select { diff --git a/pkg/client/rp/device.go b/pkg/client/rp/device.go index ec4dabe..c2d1f8a 100644 --- a/pkg/client/rp/device.go +++ b/pkg/client/rp/device.go @@ -33,7 +33,7 @@ func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc. // in RFC 8628, section 3.1 and 3.2: // https://www.rfc-editor.org/rfc/rfc8628#section-3.1 func DeviceAuthorization(ctx context.Context, scopes []string, rp RelyingParty, authFn any) (*oidc.DeviceAuthorizationResponse, error) { - ctx, span := tracer.Start(ctx, "DeviceAuthorization") + ctx, span := client.Tracer.Start(ctx, "DeviceAuthorization") defer span.End() ctx = logCtxWithRPData(ctx, rp, "function", "DeviceAuthorization") @@ -49,7 +49,7 @@ func DeviceAuthorization(ctx context.Context, scopes []string, rp RelyingParty, // by means of polling as defined in RFC, section 3.3 and 3.4: // https://www.rfc-editor.org/rfc/rfc8628#section-3.4 func DeviceAccessToken(ctx context.Context, deviceCode string, interval time.Duration, rp RelyingParty) (resp *oidc.AccessTokenResponse, err error) { - ctx, span := tracer.Start(ctx, "DeviceAccessToken") + ctx, span := client.Tracer.Start(ctx, "DeviceAccessToken") defer span.End() ctx = logCtxWithRPData(ctx, rp, "function", "DeviceAccessToken") diff --git a/pkg/client/rp/jwks.go b/pkg/client/rp/jwks.go index a3c1647..a061777 100644 --- a/pkg/client/rp/jwks.go +++ b/pkg/client/rp/jwks.go @@ -9,6 +9,7 @@ import ( jose "github.com/go-jose/go-jose/v3" + "github.com/zitadel/oidc/v3/pkg/client" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" ) @@ -83,7 +84,7 @@ func (i *inflight) result() ([]jose.JSONWebKey, error) { } func (r *remoteKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { - ctx, span := tracer.Start(ctx, "VerifySignature") + ctx, span := client.Tracer.Start(ctx, "VerifySignature") defer span.End() keyID, alg := oidc.GetKeyIDAndAlg(jws) @@ -138,7 +139,7 @@ func (r *remoteKeySet) exactMatch(jwkID, jwsID string) bool { } func (r *remoteKeySet) verifySignatureRemote(ctx context.Context, jws *jose.JSONWebSignature, keyID, alg string) ([]byte, error) { - ctx, span := tracer.Start(ctx, "verifySignatureRemote") + ctx, span := client.Tracer.Start(ctx, "verifySignatureRemote") defer span.End() keys, err := r.keysFromRemote(ctx) @@ -165,7 +166,7 @@ func (r *remoteKeySet) keysFromCache() (keys []jose.JSONWebKey) { // keysFromRemote syncs the key set from the remote set, records the values in the // cache, and returns the key set. func (r *remoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, error) { - ctx, span := tracer.Start(ctx, "keysFromRemote") + ctx, span := client.Tracer.Start(ctx, "keysFromRemote") defer span.End() // Need to lock to inspect the inflight request field. @@ -191,7 +192,7 @@ func (r *remoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, e } func (r *remoteKeySet) updateKeys(ctx context.Context) { - ctx, span := tracer.Start(ctx, "updateKeys") + ctx, span := client.Tracer.Start(ctx, "updateKeys") defer span.End() // Sync keys and finish inflight when that's done. @@ -213,7 +214,7 @@ func (r *remoteKeySet) updateKeys(ctx context.Context) { } func (r *remoteKeySet) fetchRemoteKeys(ctx context.Context) ([]jose.JSONWebKey, error) { - ctx, span := tracer.Start(ctx, "fetchRemoteKeys") + ctx, span := client.Tracer.Start(ctx, "fetchRemoteKeys") defer span.End() req, err := http.NewRequest("GET", r.jwksURL, nil) diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 3022035..62c650e 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -11,12 +11,10 @@ import ( "github.com/go-jose/go-jose/v3" "github.com/google/uuid" - "github.com/zitadel/logging" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/trace" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" + "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/client" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" @@ -30,12 +28,6 @@ const ( var ErrUserInfoSubNotMatching = errors.New("sub from userinfo does not match the sub from the id_token") -var tracer trace.Tracer - -func init() { - tracer = otel.Tracer("github.com/zitadel/oidc/pkg/client/rp") -} - // RelyingParty declares the minimal interface for oidc clients type RelyingParty interface { // OAuthConfig returns the oauth2 Config @@ -436,7 +428,7 @@ func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (stri var ErrMissingIDToken = errors.New("id_token missing") func verifyTokenResponse[C oidc.IDClaims](ctx context.Context, token *oauth2.Token, rp RelyingParty) (*oidc.Tokens[C], error) { - ctx, span := tracer.Start(ctx, "verifyTokenResponse") + ctx, span := client.Tracer.Start(ctx, "verifyTokenResponse") defer span.End() if rp.IsOAuth2Only() { @@ -456,7 +448,7 @@ func verifyTokenResponse[C oidc.IDClaims](ctx context.Context, token *oauth2.Tok // CodeExchange handles the oauth2 code exchange, extracting and validating the id_token // returning it parsed together with the oauth2 tokens (access, refresh) func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens[C], err error) { - ctx, codeExchangeSpan := tracer.Start(ctx, "CodeExchange") + ctx, codeExchangeSpan := client.Tracer.Start(ctx, "CodeExchange") defer codeExchangeSpan.End() ctx = logCtxWithRPData(ctx, rp, "function", "CodeExchange") @@ -466,7 +458,7 @@ func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingP codeOpts = append(codeOpts, opt()...) } - ctx, oauthExchangeSpan := tracer.Start(ctx, "OAuthExchange") + ctx, oauthExchangeSpan := client.Tracer.Start(ctx, "OAuthExchange") token, err := rp.OAuthConfig().Exchange(ctx, code, codeOpts...) if err != nil { return nil, err @@ -485,7 +477,7 @@ func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingP // [RFC 6749, section 4.4]: https://datatracker.ietf.org/doc/html/rfc6749#section-4.4 func ClientCredentials(ctx context.Context, rp RelyingParty, endpointParams url.Values) (token *oauth2.Token, err error) { ctx = logCtxWithRPData(ctx, rp, "function", "ClientCredentials") - ctx, span := tracer.Start(ctx, "ClientCredentials") + ctx, span := client.Tracer.Start(ctx, "ClientCredentials") defer span.End() ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient()) @@ -508,7 +500,7 @@ type CodeExchangeCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.R // Custom parameters can optionally be set to the token URL. func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "CodeExchangeHandler") + ctx, span := client.Tracer.Start(r.Context(), "CodeExchangeHandler") r = r.WithContext(ctx) defer span.End() @@ -563,7 +555,7 @@ type CodeExchangeUserinfoCallback[C oidc.IDClaims, U SubjectGetter] func(w http. // on success it will pass the userinfo into its callback function as well func UserinfoCallback[C oidc.IDClaims, U SubjectGetter](f CodeExchangeUserinfoCallback[C, U]) CodeExchangeCallback[C] { return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) { - ctx, span := tracer.Start(r.Context(), "UserinfoCallback") + ctx, span := client.Tracer.Start(r.Context(), "UserinfoCallback") r = r.WithContext(ctx) defer span.End() @@ -585,7 +577,7 @@ func UserinfoCallback[C oidc.IDClaims, U SubjectGetter](f CodeExchangeUserinfoCa func Userinfo[U SubjectGetter](ctx context.Context, token, tokenType, subject string, rp RelyingParty) (userinfo U, err error) { var nilU U ctx = logCtxWithRPData(ctx, rp, "function", "Userinfo") - ctx, span := tracer.Start(ctx, "Userinfo") + ctx, span := client.Tracer.Start(ctx, "Userinfo") defer span.End() req, err := http.NewRequestWithContext(ctx, http.MethodGet, rp.UserinfoEndpoint(), nil) @@ -745,7 +737,7 @@ type RefreshTokenRequest struct { // the IDToken and AccessToken will be verfied // and the IDToken and IDTokenClaims fields will be populated in the returned object. func RefreshTokens[C oidc.IDClaims](ctx context.Context, rp RelyingParty, refreshToken, clientAssertion, clientAssertionType string) (*oidc.Tokens[C], error) { - ctx, span := tracer.Start(ctx, "RefreshTokens") + ctx, span := client.Tracer.Start(ctx, "RefreshTokens") defer span.End() ctx = logCtxWithRPData(ctx, rp, "function", "RefreshTokens") @@ -773,7 +765,7 @@ func RefreshTokens[C oidc.IDClaims](ctx context.Context, rp RelyingParty, refres func EndSession(ctx context.Context, rp RelyingParty, idToken, optionalRedirectURI, optionalState string) (*url.URL, error) { ctx = logCtxWithRPData(ctx, rp, "function", "EndSession") - ctx, span := tracer.Start(ctx, "RefreshTokens") + ctx, span := client.Tracer.Start(ctx, "RefreshTokens") defer span.End() request := oidc.EndSessionRequest{ @@ -792,7 +784,7 @@ func EndSession(ctx context.Context, rp RelyingParty, idToken, optionalRedirectU // tokenTypeHint should be either "id_token" or "refresh_token". func RevokeToken(ctx context.Context, rp RelyingParty, token string, tokenTypeHint string) error { ctx = logCtxWithRPData(ctx, rp, "function", "RevokeToken") - ctx, span := tracer.Start(ctx, "RefreshTokens") + ctx, span := client.Tracer.Start(ctx, "RefreshTokens") defer span.End() request := client.RevokeRequest{ Token: token, diff --git a/pkg/client/rp/verifier.go b/pkg/client/rp/verifier.go index ebc10cc..94be079 100644 --- a/pkg/client/rp/verifier.go +++ b/pkg/client/rp/verifier.go @@ -6,13 +6,14 @@ import ( jose "github.com/go-jose/go-jose/v3" + "github.com/zitadel/oidc/v3/pkg/client" "github.com/zitadel/oidc/v3/pkg/oidc" ) // VerifyTokens implement the Token Response Validation as defined in OIDC specification // https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken string, v *IDTokenVerifier) (claims C, err error) { - ctx, span := tracer.Start(ctx, "VerifyTokens") + ctx, span := client.Tracer.Start(ctx, "VerifyTokens") defer span.End() var nilClaims C @@ -30,7 +31,7 @@ func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken str // VerifyIDToken validates the id token according to // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v *IDTokenVerifier) (claims C, err error) { - ctx, span := tracer.Start(ctx, "VerifyIDToken") + ctx, span := client.Tracer.Start(ctx, "VerifyIDToken") defer span.End() var nilClaims C diff --git a/pkg/client/rs/resource_server.go b/pkg/client/rs/resource_server.go index 57925d5..962af7e 100644 --- a/pkg/client/rs/resource_server.go +++ b/pkg/client/rs/resource_server.go @@ -123,6 +123,9 @@ func WithStaticEndpoints(tokenURL, introspectURL string) Option { // // [RFC7662]: https://www.rfc-editor.org/rfc/rfc7662 func Introspect[R any](ctx context.Context, rp ResourceServer, token string) (resp R, err error) { + ctx, span := client.Tracer.Start(ctx, "Introspect") + defer span.End() + if rp.IntrospectionURL() == "" { return resp, errors.New("resource server: introspection URL is empty") } diff --git a/pkg/client/tokenexchange/tokenexchange.go b/pkg/client/tokenexchange/tokenexchange.go index fdac833..c62f38c 100644 --- a/pkg/client/tokenexchange/tokenexchange.go +++ b/pkg/client/tokenexchange/tokenexchange.go @@ -101,6 +101,9 @@ func ExchangeToken( Scopes []string, RequestedTokenType oidc.TokenType, ) (*oidc.TokenExchangeResponse, error) { + ctx, span := client.Tracer.Start(ctx, "ExchangeToken") + defer span.End() + if SubjectToken == "" { return nil, errors.New("empty subject_token") } diff --git a/pkg/op/op.go b/pkg/op/op.go index 326737a..3248317 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -12,7 +12,6 @@ import ( "github.com/rs/cors" "github.com/zitadel/schema" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/trace" "golang.org/x/text/language" httphelper "github.com/zitadel/oidc/v3/pkg/http" @@ -97,11 +96,7 @@ var ( } ) -var tracer trace.Tracer - -func init() { - tracer = otel.Tracer("github.com/zitadel/oidc/pkg/op") -} +var tracer = otel.Tracer("github.com/zitadel/oidc/pkg/op") type OpenIDProvider interface { http.Handler From 03f3bc693b04e2224515a112f866fd00ec2af4cc Mon Sep 17 00:00:00 2001 From: adlerhurst Date: Thu, 14 Mar 2024 07:50:29 +0100 Subject: [PATCH 370/502] fix test --- pkg/client/rs/resource_server_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/client/rs/resource_server_test.go b/pkg/client/rs/resource_server_test.go index bb17c64..7a5ced9 100644 --- a/pkg/client/rs/resource_server_test.go +++ b/pkg/client/rs/resource_server_test.go @@ -201,7 +201,7 @@ func TestIntrospect(t *testing.T) { { name: "missing-introspect-url", args: args{ - ctx: nil, + ctx: context.Background(), rp: rp, token: "my-token", }, From 9afc07c0cbd3d3752a07173ed62b3d3ac31db4a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Mar 2024 06:55:56 +0000 Subject: [PATCH 371/502] chore(deps): bump google.golang.org/protobuf from 1.31.0 to 1.33.0 (#568) Bumps google.golang.org/protobuf from 1.31.0 to 1.33.0. --- updated-dependencies: - dependency-name: google.golang.org/protobuf dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index c7562ea..bd7a0e5 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,6 @@ require ( github.com/zitadel/logging v0.6.0 github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.24.0 - go.opentelemetry.io/otel/trace v1.24.0 golang.org/x/oauth2 v0.18.0 golang.org/x/text v0.14.0 ) @@ -32,10 +31,11 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/net v0.22.0 // indirect golang.org/x/sys v0.18.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2b1eb7e..58029da 100644 --- a/go.sum +++ b/go.sum @@ -136,8 +136,8 @@ google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 4d63d68c9e145dd79269130474984e01e8ef6530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Thu, 14 Mar 2024 08:57:44 +0200 Subject: [PATCH 372/502] feat(op): allow setting the actor to Token Requests (#569) For impersonation token exchange we need to persist the actor throughout token requests, including refresh token. This PR adds the optional TokenActorRequest interface which allows to pass such actor. --- pkg/op/token.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkg/op/token.go b/pkg/op/token.go index 19edcce..b45789b 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -121,6 +121,10 @@ func CreateBearerToken(tokenID, subject string, crypto Crypto) (string, error) { return crypto.Encrypt(tokenID + ":" + subject) } +type TokenActorRequest interface { + GetActor() *oidc.ActorClaims +} + func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, exp time.Time, id string, client AccessTokenClient, storage Storage) (string, error) { ctx, span := tracer.Start(ctx, "CreateJWT") defer span.End() @@ -150,6 +154,9 @@ func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, ex } claims.Claims = privateClaims } + if actorReq, ok := tokenRequest.(TokenActorRequest); ok { + claims.Actor = actorReq.GetActor() + } signingKey, err := storage.SigningKey(ctx) if err != nil { return "", err @@ -181,6 +188,10 @@ func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, v nonce = authRequest.GetNonce() } claims := oidc.NewIDTokenClaims(issuer, request.GetSubject(), request.GetAudience(), exp, request.GetAuthTime(), nonce, acr, request.GetAMR(), request.GetClientID(), client.ClockSkew()) + if actorReq, ok := request.(TokenActorRequest); ok { + claims.Actor = actorReq.GetActor() + } + scopes := client.RestrictAdditionalIdTokenScopes()(request.GetScopes()) signingKey, err := storage.SigningKey(ctx) if err != nil { From 56397f88d5b8abcda55ead200af66093e428786e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Mon, 18 Mar 2024 12:36:16 +0200 Subject: [PATCH 373/502] feat(oidc): add actor claim to introspection response (#570) With impersonation we assign an actor claim to our JWT/ID Tokens. This change adds the actor claim to the introspection response to follow suit. This PR also adds the `auth_time` and `amr` claims for consistency. --- example/client/app/app.go | 4 ++++ pkg/oidc/introspection.go | 27 +++++++++++++++------------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/example/client/app/app.go b/example/client/app/app.go index 9b43b8d..99aba3d 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -99,6 +99,10 @@ func main() { // for demonstration purposes the returned userinfo response is written as JSON object onto response marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) { + fmt.Println("access token", tokens.AccessToken) + fmt.Println("refresh token", tokens.RefreshToken) + fmt.Println("id token", tokens.IDToken) + data, err := json.Marshal(info) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index 8313dc4..1a200eb 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -16,18 +16,21 @@ type ClientAssertionParams struct { // https://www.rfc-editor.org/rfc/rfc7662.html#section-2.2. // https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims. type IntrospectionResponse struct { - Active bool `json:"active"` - Scope SpaceDelimitedArray `json:"scope,omitempty"` - ClientID string `json:"client_id,omitempty"` - TokenType string `json:"token_type,omitempty"` - Expiration Time `json:"exp,omitempty"` - IssuedAt Time `json:"iat,omitempty"` - NotBefore Time `json:"nbf,omitempty"` - Subject string `json:"sub,omitempty"` - Audience Audience `json:"aud,omitempty"` - Issuer string `json:"iss,omitempty"` - JWTID string `json:"jti,omitempty"` - Username string `json:"username,omitempty"` + Active bool `json:"active"` + Scope SpaceDelimitedArray `json:"scope,omitempty"` + ClientID string `json:"client_id,omitempty"` + TokenType string `json:"token_type,omitempty"` + Expiration Time `json:"exp,omitempty"` + IssuedAt Time `json:"iat,omitempty"` + AuthTime Time `json:"auth_time,omitempty"` + NotBefore Time `json:"nbf,omitempty"` + Subject string `json:"sub,omitempty"` + Audience Audience `json:"aud,omitempty"` + AuthenticationMethodsReferences []string `json:"amr,omitempty"` + Issuer string `json:"iss,omitempty"` + JWTID string `json:"jti,omitempty"` + Username string `json:"username,omitempty"` + Actor *ActorClaims `json:"act,omitempty"` UserInfoProfile UserInfoEmail UserInfoPhone From 910f55ea7bae83ec053de8218eeed6f65da46212 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Mar 2024 07:15:38 +0100 Subject: [PATCH 374/502] chore(deps): bump actions/add-to-project from 0.6.0 to 0.6.1 (#572) Bumps [actions/add-to-project](https://github.com/actions/add-to-project) from 0.6.0 to 0.6.1. - [Release notes](https://github.com/actions/add-to-project/releases) - [Commits](https://github.com/actions/add-to-project/compare/v0.6.0...v0.6.1) --- updated-dependencies: - dependency-name: actions/add-to-project dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/issue.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index a2b56eb..1138d78 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: add issue - uses: actions/add-to-project@v0.6.0 + uses: actions/add-to-project@v0.6.1 if: ${{ github.event_name == 'issues' }} with: # You can target a repository in a different organization @@ -28,7 +28,7 @@ jobs: username: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_PAT }} - name: add pr - uses: actions/add-to-project@v0.6.0 + uses: actions/add-to-project@v0.6.1 if: ${{ github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'engineers')}} with: # You can target a repository in a different organization From c89d0ed97073ca5d23e0a793efca80a9bfc9535e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9lian=20GARCIA?= Date: Mon, 1 Apr 2024 15:55:22 +0200 Subject: [PATCH 375/502] feat: return oidc.Error in case of call token failure (#571) --- pkg/client/client.go | 21 +++------------------ pkg/http/http.go | 9 ++++++++- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index 8b60264..78b412a 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -2,7 +2,6 @@ package client import ( "context" - "encoding/json" "errors" "fmt" "io" @@ -251,25 +250,11 @@ func CallDeviceAccessTokenEndpoint(ctx context.Context, request *DeviceAccessTok req.SetBasicAuth(request.ClientID, request.ClientSecret) } - httpResp, err := caller.HttpClient().Do(req) - if err != nil { + resp := new(oidc.AccessTokenResponse) + if err := httphelper.HttpRequest(caller.HttpClient(), req, &resp); err != nil { return nil, err } - defer httpResp.Body.Close() - - resp := new(struct { - *oidc.AccessTokenResponse - *oidc.Error - }) - if err = json.NewDecoder(httpResp.Body).Decode(resp); err != nil { - return nil, err - } - - if httpResp.StatusCode == http.StatusOK { - return resp.AccessTokenResponse, nil - } - - return nil, resp.Error + return resp, nil } func PollDeviceAccessTokenEndpoint(ctx context.Context, interval time.Duration, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) { diff --git a/pkg/http/http.go b/pkg/http/http.go index fd196b0..33c5f15 100644 --- a/pkg/http/http.go +++ b/pkg/http/http.go @@ -10,6 +10,8 @@ import ( "net/url" "strings" "time" + + "github.com/zitadel/oidc/v3/pkg/oidc" ) var DefaultHTTPClient = &http.Client{ @@ -66,7 +68,12 @@ func HttpRequest(client *http.Client, req *http.Request, response any) error { } if resp.StatusCode != http.StatusOK { - return fmt.Errorf("http status not ok: %s %s", resp.Status, body) + var oidcErr oidc.Error + err = json.Unmarshal(body, &oidcErr) + if err != nil || oidcErr.ErrorType == "" { + return fmt.Errorf("http status not ok: %s %s", resp.Status, body) + } + return &oidcErr } err = json.Unmarshal(body, response) From d729c225265df9f307e82515fc332f553b0b440c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 07:58:28 +0200 Subject: [PATCH 376/502] chore(deps): bump codecov/codecov-action from 4.1.0 to 4.1.1 (#576) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.1.0 to 4.1.1. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4.1.0...v4.1.1) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4a8f87..202596f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v4.1.0 + - uses: codecov/codecov-action@v4.1.1 with: file: ./profile.cov name: codecov-go From 5cdb65c30b5524716a8096bfbafa26a101fe3acf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 06:22:36 +0000 Subject: [PATCH 377/502] chore(deps): bump actions/add-to-project from 0.6.1 to 1.0.0 (#575) * chore(deps): bump actions/add-to-project from 0.6.1 to 1.0.0 Bumps [actions/add-to-project](https://github.com/actions/add-to-project) from 0.6.1 to 1.0.0. - [Release notes](https://github.com/actions/add-to-project/releases) - [Commits](https://github.com/actions/add-to-project/compare/v0.6.1...v1.0.0) --- updated-dependencies: - dependency-name: actions/add-to-project dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Update issue.yml --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Livio Spring --- .github/workflows/issue.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 1138d78..d328058 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: add issue - uses: actions/add-to-project@v0.6.1 + uses: actions/add-to-project@v1 if: ${{ github.event_name == 'issues' }} with: # You can target a repository in a different organization @@ -28,7 +28,7 @@ jobs: username: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_PAT }} - name: add pr - uses: actions/add-to-project@v0.6.1 + uses: actions/add-to-project@v1 if: ${{ github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'engineers')}} with: # You can target a repository in a different organization From a3b73a6950a8b846a6912f64c2b4f5a12b506a83 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Wed, 3 Apr 2024 18:32:50 +0200 Subject: [PATCH 378/502] chore(workflow): fix action/add-to-project version (#578) --- .github/workflows/issue.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index d328058..5926c59 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: add issue - uses: actions/add-to-project@v1 + uses: actions/add-to-project@v1.0.0 if: ${{ github.event_name == 'issues' }} with: # You can target a repository in a different organization @@ -28,7 +28,7 @@ jobs: username: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_PAT }} - name: add pr - uses: actions/add-to-project@v1 + uses: actions/add-to-project@v1.0.0 if: ${{ github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'engineers')}} with: # You can target a repository in a different organization From 370738772ac77348dd1ef3de9984edffb8857db3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 10:52:08 +0300 Subject: [PATCH 379/502] chore(deps): bump golang.org/x/oauth2 from 0.18.0 to 0.19.0 (#580) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.18.0 to 0.19.0. - [Commits](https://github.com/golang/oauth2/compare/v0.18.0...v0.19.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 5 +---- go.sum | 17 ++--------------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index bd7a0e5..35350de 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/zitadel/logging v0.6.0 github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.24.0 - golang.org/x/oauth2 v0.18.0 + golang.org/x/oauth2 v0.19.0 golang.org/x/text v0.14.0 ) @@ -27,7 +27,6 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect @@ -35,7 +34,5 @@ require ( golang.org/x/crypto v0.21.0 // indirect golang.org/x/net v0.22.0 // indirect golang.org/x/sys v0.18.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 58029da..5a96136 100644 --- a/go.sum +++ b/go.sum @@ -14,13 +14,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -78,7 +73,6 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -89,8 +83,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= -golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= +golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= +golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -115,7 +109,6 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= @@ -132,12 +125,6 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 33485b82baf39e91577f494396ce46bb7678a1fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 10:57:09 +0300 Subject: [PATCH 380/502] chore(deps): bump go.opentelemetry.io/otel from 1.24.0 to 1.25.0 (#584) Bumps [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) from 1.24.0 to 1.25.0. - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.24.0...v1.25.0) --- updated-dependencies: - dependency-name: go.opentelemetry.io/otel dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 35350de..c23916c 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/stretchr/testify v1.9.0 github.com/zitadel/logging v0.6.0 github.com/zitadel/schema v1.3.0 - go.opentelemetry.io/otel v1.24.0 + go.opentelemetry.io/otel v1.25.0 golang.org/x/oauth2 v0.19.0 golang.org/x/text v0.14.0 ) @@ -29,8 +29,8 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.25.0 // indirect + go.opentelemetry.io/otel/trace v1.25.0 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/net v0.22.0 // indirect golang.org/x/sys v0.18.0 // indirect diff --git a/go.sum b/go.sum index 5a96136..179b21c 100644 --- a/go.sum +++ b/go.sum @@ -56,12 +56,12 @@ github.com/zitadel/logging v0.6.0 h1:t5Nnt//r+m2ZhhoTmoPX+c96pbMarqJvW1Vq6xFTank github.com/zitadel/logging v0.6.0/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k= +go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg= +go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA= +go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s= +go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM= +go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= From e75a061807518e681dd803c38f1df5add504a7ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9lian=20GARCIA?= Date: Mon, 8 Apr 2024 15:43:31 +0200 Subject: [PATCH 381/502] feat: support verification_url workaround for DeviceAuthorizationResponse unmarshal (#577) --- pkg/oidc/device_authorization.go | 22 ++++++++++++++++++++ pkg/oidc/device_authorization_test.go | 30 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 pkg/oidc/device_authorization_test.go diff --git a/pkg/oidc/device_authorization.go b/pkg/oidc/device_authorization.go index 68b8efa..a6417ba 100644 --- a/pkg/oidc/device_authorization.go +++ b/pkg/oidc/device_authorization.go @@ -1,5 +1,7 @@ package oidc +import "encoding/json" + // DeviceAuthorizationRequest implements // https://www.rfc-editor.org/rfc/rfc8628#section-3.1, // 3.1 Device Authorization Request. @@ -20,6 +22,26 @@ type DeviceAuthorizationResponse struct { Interval int `json:"interval,omitempty"` } +func (resp *DeviceAuthorizationResponse) UnmarshalJSON(data []byte) error { + type Alias DeviceAuthorizationResponse + aux := &struct { + // workaround misspelling of verification_uri + // https://stackoverflow.com/q/76696956/5690223 + // https://developers.google.com/identity/protocols/oauth2/limited-input-device?hl=fr#success-response + VerificationURL string `json:"verification_url"` + *Alias + }{ + Alias: (*Alias)(resp), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + if resp.VerificationURI == "" { + resp.VerificationURI = aux.VerificationURL + } + return nil +} + // DeviceAccessTokenRequest implements // https://www.rfc-editor.org/rfc/rfc8628#section-3.4, // Device Access Token Request. diff --git a/pkg/oidc/device_authorization_test.go b/pkg/oidc/device_authorization_test.go new file mode 100644 index 0000000..c4c6637 --- /dev/null +++ b/pkg/oidc/device_authorization_test.go @@ -0,0 +1,30 @@ +package oidc + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDeviceAuthorizationResponse_UnmarshalJSON(t *testing.T) { + jsonStr := `{ + "device_code": "deviceCode", + "user_code": "userCode", + "verification_url": "http://example.com/verify", + "expires_in": 3600, + "interval": 5 + }` + + expected := &DeviceAuthorizationResponse{ + DeviceCode: "deviceCode", + UserCode: "userCode", + VerificationURI: "http://example.com/verify", + ExpiresIn: 3600, + Interval: 5, + } + + var resp DeviceAuthorizationResponse + err := resp.UnmarshalJSON([]byte(jsonStr)) + assert.NoError(t, err) + assert.Equal(t, expected, &resp) +} From 8a21d3813610c0dc64b349de3d7d01b2480120f0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Apr 2024 12:39:36 +0300 Subject: [PATCH 382/502] chore(deps): bump codecov/codecov-action from 4.1.1 to 4.2.0 (#585) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.1.1 to 4.2.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4.1.1...v4.2.0) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 202596f..8f3c1da 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v4.1.1 + - uses: codecov/codecov-action@v4.2.0 with: file: ./profile.cov name: codecov-go From 06f37f84c1a9263bab1a1350afdbd65502e4a0c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Tue, 9 Apr 2024 15:02:31 +0200 Subject: [PATCH 383/502] fix: Fail safe, if optional endpoints are not given (#582) --- pkg/client/client.go | 23 +++++++++++++++++++---- pkg/client/errors.go | 5 +++++ 2 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 pkg/client/errors.go diff --git a/pkg/client/client.go b/pkg/client/client.go index 78b412a..a65b7b8 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -10,7 +10,7 @@ import ( "strings" "time" - jose "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v3" "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/crypto" httphelper "github.com/zitadel/oidc/v3/pkg/http" @@ -97,7 +97,12 @@ func CallEndSessionEndpoint(ctx context.Context, request any, authFn any, caller ctx, span := Tracer.Start(ctx, "CallEndSessionEndpoint") defer span.End() - req, err := httphelper.FormRequest(ctx, caller.GetEndSessionEndpoint(), request, Encoder, authFn) + endpoint := caller.GetEndSessionEndpoint() + if endpoint == "" { + return nil, fmt.Errorf("end session %w", ErrEndpointNotSet) + } + + req, err := httphelper.FormRequest(ctx, endpoint, request, Encoder, authFn) if err != nil { return nil, err } @@ -143,7 +148,12 @@ func CallRevokeEndpoint(ctx context.Context, request any, authFn any, caller Rev ctx, span := Tracer.Start(ctx, "CallRevokeEndpoint") defer span.End() - req, err := httphelper.FormRequest(ctx, caller.GetRevokeEndpoint(), request, Encoder, authFn) + endpoint := caller.GetRevokeEndpoint() + if endpoint == "" { + return fmt.Errorf("revoke %w", ErrEndpointNotSet) + } + + req, err := httphelper.FormRequest(ctx, endpoint, request, Encoder, authFn) if err != nil { return err } @@ -218,7 +228,12 @@ func CallDeviceAuthorizationEndpoint(ctx context.Context, request *oidc.ClientCr ctx, span := Tracer.Start(ctx, "CallDeviceAuthorizationEndpoint") defer span.End() - req, err := httphelper.FormRequest(ctx, caller.GetDeviceAuthorizationEndpoint(), request, Encoder, authFn) + endpoint := caller.GetDeviceAuthorizationEndpoint() + if endpoint == "" { + return nil, fmt.Errorf("device authorization %w", ErrEndpointNotSet) + } + + req, err := httphelper.FormRequest(ctx, endpoint, request, Encoder, authFn) if err != nil { return nil, err } diff --git a/pkg/client/errors.go b/pkg/client/errors.go new file mode 100644 index 0000000..47210e5 --- /dev/null +++ b/pkg/client/errors.go @@ -0,0 +1,5 @@ +package client + +import "errors" + +var ErrEndpointNotSet = errors.New("endpoint not set") From 33f8df7eb28f19fd27ffd8ad12390b3cc74a247a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Thu, 11 Apr 2024 18:13:30 +0300 Subject: [PATCH 384/502] feat(deps): update go-jose to v4 (#588) This change updates to go-jose v4, which was a new major release. jose.ParseSigned now expects the supported signing algorithms to be passed, on which we previously did our own check. As they use a dedicated type for this, the slice of string needs to be converted. The returned error also need to be handled in a non-standard way in order to stay compatible. For OIDC v4 we should use the jose.SignatureAlgorithm type directly and wrap errors, instead of returned static defined errors. Closes #583 --- example/server/storage/storage.go | 2 +- example/server/storage/storage_dynamic.go | 2 +- go.mod | 6 ++-- go.sum | 39 ++++------------------- internal/testutil/token.go | 2 +- pkg/client/client.go | 2 +- pkg/client/profile/jwt_profile.go | 2 +- pkg/client/rp/jwks.go | 2 +- pkg/client/rp/relying_party.go | 2 +- pkg/client/rp/verifier.go | 2 +- pkg/client/rp/verifier_test.go | 2 +- pkg/client/tokenexchange/tokenexchange.go | 2 +- pkg/crypto/hash.go | 2 +- pkg/crypto/sign.go | 2 +- pkg/oidc/keyset.go | 2 +- pkg/oidc/keyset_test.go | 2 +- pkg/oidc/token.go | 2 +- pkg/oidc/token_request.go | 2 +- pkg/oidc/token_test.go | 2 +- pkg/oidc/types.go | 2 +- pkg/oidc/verifier.go | 27 +++++++++++----- pkg/op/discovery.go | 2 +- pkg/op/discovery_test.go | 2 +- pkg/op/keys.go | 2 +- pkg/op/keys_test.go | 2 +- pkg/op/mock/authorizer.mock.impl.go | 2 +- pkg/op/mock/discovery.mock.go | 2 +- pkg/op/mock/signer.mock.go | 2 +- pkg/op/mock/storage.mock.go | 2 +- pkg/op/op.go | 2 +- pkg/op/signer.go | 2 +- pkg/op/storage.go | 2 +- pkg/op/verifier_jwt_profile.go | 2 +- 33 files changed, 58 insertions(+), 74 deletions(-) diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index b556828..d8b7a5d 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -11,7 +11,7 @@ import ( "sync" "time" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" "github.com/google/uuid" "github.com/zitadel/oidc/v3/pkg/oidc" diff --git a/example/server/storage/storage_dynamic.go b/example/server/storage/storage_dynamic.go index a08f60e..d112d71 100644 --- a/example/server/storage/storage_dynamic.go +++ b/example/server/storage/storage_dynamic.go @@ -4,7 +4,7 @@ import ( "context" "time" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" diff --git a/go.mod b/go.mod index c23916c..17efbc3 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 require ( github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/go-chi/chi/v5 v5.0.12 - github.com/go-jose/go-jose/v3 v3.0.3 + github.com/go-jose/go-jose/v4 v4.0.1 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 github.com/google/uuid v1.6.0 @@ -31,8 +31,8 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/otel/metric v1.25.0 // indirect go.opentelemetry.io/otel/trace v1.25.0 // indirect - golang.org/x/crypto v0.21.0 // indirect + golang.org/x/crypto v0.22.0 // indirect golang.org/x/net v0.22.0 // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/sys v0.19.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 179b21c..0f48ad9 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ 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/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= -github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= +github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -16,7 +16,6 @@ github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo= @@ -51,7 +50,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zitadel/logging v0.6.0 h1:t5Nnt//r+m2ZhhoTmoPX+c96pbMarqJvW1Vq6xFTank= github.com/zitadel/logging v0.6.0/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= @@ -64,22 +62,14 @@ go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1 go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -87,39 +77,22 @@ golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/testutil/token.go b/internal/testutil/token.go index 2dd788f..7ad8893 100644 --- a/internal/testutil/token.go +++ b/internal/testutil/token.go @@ -8,7 +8,7 @@ import ( "errors" "time" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" "github.com/muhlemmer/gu" "github.com/zitadel/oidc/v3/pkg/oidc" ) diff --git a/pkg/client/client.go b/pkg/client/client.go index a65b7b8..e17c70a 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v4" "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/crypto" httphelper "github.com/zitadel/oidc/v3/pkg/http" diff --git a/pkg/client/profile/jwt_profile.go b/pkg/client/profile/jwt_profile.go index a24033c..060f390 100644 --- a/pkg/client/profile/jwt_profile.go +++ b/pkg/client/profile/jwt_profile.go @@ -5,7 +5,7 @@ import ( "net/http" "time" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" "golang.org/x/oauth2" "github.com/zitadel/oidc/v3/pkg/client" diff --git a/pkg/client/rp/jwks.go b/pkg/client/rp/jwks.go index a061777..4a8c41b 100644 --- a/pkg/client/rp/jwks.go +++ b/pkg/client/rp/jwks.go @@ -7,7 +7,7 @@ import ( "net/http" "sync" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" "github.com/zitadel/oidc/v3/pkg/client" httphelper "github.com/zitadel/oidc/v3/pkg/http" diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 62c650e..7075eb1 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -9,7 +9,7 @@ import ( "net/url" "time" - "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v4" "github.com/google/uuid" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" diff --git a/pkg/client/rp/verifier.go b/pkg/client/rp/verifier.go index 94be079..5a07d8a 100644 --- a/pkg/client/rp/verifier.go +++ b/pkg/client/rp/verifier.go @@ -4,7 +4,7 @@ import ( "context" "time" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" "github.com/zitadel/oidc/v3/pkg/client" "github.com/zitadel/oidc/v3/pkg/oidc" diff --git a/pkg/client/rp/verifier_test.go b/pkg/client/rp/verifier_test.go index ea15c21..cd2fab4 100644 --- a/pkg/client/rp/verifier_test.go +++ b/pkg/client/rp/verifier_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" tu "github.com/zitadel/oidc/v3/internal/testutil" diff --git a/pkg/client/tokenexchange/tokenexchange.go b/pkg/client/tokenexchange/tokenexchange.go index a2ea1bb..61975a4 100644 --- a/pkg/client/tokenexchange/tokenexchange.go +++ b/pkg/client/tokenexchange/tokenexchange.go @@ -6,7 +6,7 @@ import ( "net/http" "time" - "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v4" "github.com/zitadel/oidc/v3/pkg/client" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" diff --git a/pkg/crypto/hash.go b/pkg/crypto/hash.go index 0ed2774..ab9f8c1 100644 --- a/pkg/crypto/hash.go +++ b/pkg/crypto/hash.go @@ -8,7 +8,7 @@ import ( "fmt" "hash" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" ) var ErrUnsupportedAlgorithm = errors.New("unsupported signing algorithm") diff --git a/pkg/crypto/sign.go b/pkg/crypto/sign.go index a197955..937a846 100644 --- a/pkg/crypto/sign.go +++ b/pkg/crypto/sign.go @@ -4,7 +4,7 @@ import ( "encoding/json" "errors" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" ) func Sign(object any, signer jose.Signer) (string, error) { diff --git a/pkg/oidc/keyset.go b/pkg/oidc/keyset.go index 6031c01..833878d 100644 --- a/pkg/oidc/keyset.go +++ b/pkg/oidc/keyset.go @@ -7,7 +7,7 @@ import ( "crypto/rsa" "errors" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" ) const ( diff --git a/pkg/oidc/keyset_test.go b/pkg/oidc/keyset_test.go index f8641f2..e01074e 100644 --- a/pkg/oidc/keyset_test.go +++ b/pkg/oidc/keyset_test.go @@ -7,7 +7,7 @@ import ( "reflect" "testing" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" ) func TestFindKey(t *testing.T) { diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index 73eb2e5..8d2880c 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -5,7 +5,7 @@ import ( "os" "time" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" "golang.org/x/oauth2" "github.com/muhlemmer/gu" diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index b07b333..f3b2ec4 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -6,7 +6,7 @@ import ( "slices" "time" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" ) const ( diff --git a/pkg/oidc/token_test.go b/pkg/oidc/token_test.go index 9f9ee2d..ccc3467 100644 --- a/pkg/oidc/token_test.go +++ b/pkg/oidc/token_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" "github.com/stretchr/testify/assert" "golang.org/x/text/language" ) diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index 0e7152c..e7292e6 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -9,7 +9,7 @@ import ( "strings" "time" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" "github.com/muhlemmer/gu" "github.com/zitadel/schema" "golang.org/x/text/language" diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go index fe28857..410b383 100644 --- a/pkg/oidc/verifier.go +++ b/pkg/oidc/verifier.go @@ -10,7 +10,7 @@ import ( "strings" "time" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" str "github.com/zitadel/oidc/v3/pkg/strings" ) @@ -148,8 +148,13 @@ func CheckAuthorizedParty(claims Claims, clientID string) error { } func CheckSignature(ctx context.Context, token string, payload []byte, claims ClaimsSignature, supportedSigAlgs []string, set KeySet) error { - jws, err := jose.ParseSigned(token) + jws, err := jose.ParseSigned(token, toJoseSignatureAlgorithms(supportedSigAlgs)) if err != nil { + if strings.HasPrefix(err.Error(), "go-jose/go-jose: unexpected signature algorithm") { + // TODO(v4): we should wrap errors instead of returning static ones. + // This is a workaround so we keep returning the same error for now. + return ErrSignatureUnsupportedAlg + } return ErrParse } if len(jws.Signatures) == 0 { @@ -159,12 +164,6 @@ func CheckSignature(ctx context.Context, token string, payload []byte, claims Cl return ErrSignatureMultiple } sig := jws.Signatures[0] - if len(supportedSigAlgs) == 0 { - supportedSigAlgs = []string{"RS256"} - } - if !str.Contains(supportedSigAlgs, sig.Header.Algorithm) { - return fmt.Errorf("%w: id token signed with unsupported algorithm, expected %q got %q", ErrSignatureUnsupportedAlg, supportedSigAlgs, sig.Header.Algorithm) - } signedPayload, err := set.VerifySignature(ctx, jws) if err != nil { @@ -180,6 +179,18 @@ func CheckSignature(ctx context.Context, token string, payload []byte, claims Cl return nil } +// TODO(v4): Use the new jose.SignatureAlgorithm type directly, instead of string. +func toJoseSignatureAlgorithms(algorithms []string) []jose.SignatureAlgorithm { + out := make([]jose.SignatureAlgorithm, len(algorithms)) + for i := range algorithms { + out[i] = jose.SignatureAlgorithm(algorithms[i]) + } + if len(out) == 0 { + out = append(out, jose.RS256) + } + return out +} + func CheckExpiration(claims Claims, offset time.Duration) error { expiration := claims.GetExpiration() if !time.Now().Add(offset).Before(expiration) { diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 7b5ecbe..cd08580 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -4,7 +4,7 @@ import ( "context" "net/http" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" diff --git a/pkg/op/discovery_test.go b/pkg/op/discovery_test.go index 84e1216..cb4cfba 100644 --- a/pkg/op/discovery_test.go +++ b/pkg/op/discovery_test.go @@ -6,7 +6,7 @@ import ( "net/http/httptest" "testing" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/op/keys.go b/pkg/op/keys.go index d55c8d1..c96c456 100644 --- a/pkg/op/keys.go +++ b/pkg/op/keys.go @@ -4,7 +4,7 @@ import ( "context" "net/http" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" httphelper "github.com/zitadel/oidc/v3/pkg/http" ) diff --git a/pkg/op/keys_test.go b/pkg/op/keys_test.go index e1a3851..3662739 100644 --- a/pkg/op/keys_test.go +++ b/pkg/op/keys_test.go @@ -7,7 +7,7 @@ import ( "net/http/httptest" "testing" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" diff --git a/pkg/op/mock/authorizer.mock.impl.go b/pkg/op/mock/authorizer.mock.impl.go index ba5082f..59e8fa3 100644 --- a/pkg/op/mock/authorizer.mock.impl.go +++ b/pkg/op/mock/authorizer.mock.impl.go @@ -4,7 +4,7 @@ import ( "context" "testing" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" "github.com/golang/mock/gomock" "github.com/zitadel/schema" diff --git a/pkg/op/mock/discovery.mock.go b/pkg/op/mock/discovery.mock.go index c5d3d3a..a27f8ef 100644 --- a/pkg/op/mock/discovery.mock.go +++ b/pkg/op/mock/discovery.mock.go @@ -8,7 +8,7 @@ import ( context "context" reflect "reflect" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" gomock "github.com/golang/mock/gomock" ) diff --git a/pkg/op/mock/signer.mock.go b/pkg/op/mock/signer.mock.go index 15718e0..e1bab91 100644 --- a/pkg/op/mock/signer.mock.go +++ b/pkg/op/mock/signer.mock.go @@ -7,7 +7,7 @@ package mock import ( reflect "reflect" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" gomock "github.com/golang/mock/gomock" ) diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index a1ce598..02a7c5c 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -9,7 +9,7 @@ import ( reflect "reflect" time "time" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" gomock "github.com/golang/mock/gomock" oidc "github.com/zitadel/oidc/v3/pkg/oidc" op "github.com/zitadel/oidc/v3/pkg/op" diff --git a/pkg/op/op.go b/pkg/op/op.go index 3248317..9fd6b30 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -8,7 +8,7 @@ import ( "time" "github.com/go-chi/chi/v5" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" "github.com/rs/cors" "github.com/zitadel/schema" "go.opentelemetry.io/otel" diff --git a/pkg/op/signer.go b/pkg/op/signer.go index b220739..5c3dd6a 100644 --- a/pkg/op/signer.go +++ b/pkg/op/signer.go @@ -3,7 +3,7 @@ package op import ( "errors" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" ) var ErrSignerCreationFailed = errors.New("signer creation failed") diff --git a/pkg/op/storage.go b/pkg/op/storage.go index a1a00ed..8488b28 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -5,7 +5,7 @@ import ( "errors" "time" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" "github.com/zitadel/oidc/v3/pkg/oidc" ) diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index ced99ad..06a7d34 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -6,7 +6,7 @@ import ( "fmt" "time" - jose "github.com/go-jose/go-jose/v3" + jose "github.com/go-jose/go-jose/v4" "github.com/zitadel/oidc/v3/pkg/oidc" ) From 3fa4891f3e1ee55482212721499d6a6ab2ecd163 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:15:39 +0300 Subject: [PATCH 385/502] chore(deps): bump actions/add-to-project from 1.0.0 to 1.0.1 (#589) Bumps [actions/add-to-project](https://github.com/actions/add-to-project) from 1.0.0 to 1.0.1. - [Release notes](https://github.com/actions/add-to-project/releases) - [Commits](https://github.com/actions/add-to-project/compare/v1.0.0...v1.0.1) --- updated-dependencies: - dependency-name: actions/add-to-project dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/issue.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 5926c59..5b1febf 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: add issue - uses: actions/add-to-project@v1.0.0 + uses: actions/add-to-project@v1.0.1 if: ${{ github.event_name == 'issues' }} with: # You can target a repository in a different organization @@ -28,7 +28,7 @@ jobs: username: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_PAT }} - name: add pr - uses: actions/add-to-project@v1.0.0 + uses: actions/add-to-project@v1.0.1 if: ${{ github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'engineers')}} with: # You can target a repository in a different organization From a77d773ca3dee5ec73dd96aed99bb303a0d66406 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 08:16:57 +0000 Subject: [PATCH 386/502] chore(deps): bump codecov/codecov-action from 4.2.0 to 4.3.0 (#590) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.2.0 to 4.3.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4.2.0...v4.3.0) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8f3c1da..a6d3826 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v4.2.0 + - uses: codecov/codecov-action@v4.3.0 with: file: ./profile.cov name: codecov-go From 959376bde763f958f3f430b6534de73fd700d7a7 Mon Sep 17 00:00:00 2001 From: Ethan Heilman Date: Tue, 16 Apr 2024 04:18:32 -0400 Subject: [PATCH 387/502] Fixes typos in GoDoc and comments (#591) --- example/server/exampleop/op.go | 2 +- pkg/client/rp/relying_party.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/example/server/exampleop/op.go b/example/server/exampleop/op.go index 893628e..e502536 100644 --- a/example/server/exampleop/op.go +++ b/example/server/exampleop/op.go @@ -56,7 +56,7 @@ func SetupServer(issuer string, storage Storage, logger *slog.Logger, wrapServer // for simplicity, we provide a very small default page for users who have signed out router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("signed out successfully")) - // no need to check/log error, this will be handeled by the middleware. + // no need to check/log error, this will be handled by the middleware. }) // creation of the OpenIDProvider with the just created in-memory Storage diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 7075eb1..bd2041b 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -60,7 +60,7 @@ type RelyingParty interface { // UserinfoEndpoint returns the userinfo UserinfoEndpoint() string - // GetDeviceAuthorizationEndpoint returns the enpoint which can + // GetDeviceAuthorizationEndpoint returns the endpoint which can // be used to start a DeviceAuthorization flow. GetDeviceAuthorizationEndpoint() string @@ -388,7 +388,7 @@ func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string { // AuthURLHandler extends the `AuthURL` method with a http redirect handler // including handling setting cookie for secure `state` transfer. -// Custom paramaters can optionally be set to the redirect URL. +// Custom parameters can optionally be set to the redirect URL. func AuthURLHandler(stateFn func() string, rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { opts := make([]AuthURLOpt, len(urlParam)) @@ -642,7 +642,7 @@ func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints { } } -// withURLParam sets custom url paramaters. +// withURLParam sets custom url parameters. // This is the generalized, unexported, function used by both // URLParamOpt and AuthURLOpt. func withURLParam(key, value string) func() []oauth2.AuthCodeOption { @@ -734,7 +734,7 @@ type RefreshTokenRequest struct { // the old one should be considered invalid. // // In case the RP is not OAuth2 only and an IDToken was part of the response, -// the IDToken and AccessToken will be verfied +// the IDToken and AccessToken will be verified // and the IDToken and IDTokenClaims fields will be populated in the returned object. func RefreshTokens[C oidc.IDClaims](ctx context.Context, rp RelyingParty, refreshToken, clientAssertion, clientAssertionType string) (*oidc.Tokens[C], error) { ctx, span := client.Tracer.Start(ctx, "RefreshTokens") From 68d4e08f6deb96fa7d39b32bf8484d72f782203d Mon Sep 17 00:00:00 2001 From: Kotaro Otaka <117251387+otakakot@users.noreply.github.com> Date: Tue, 16 Apr 2024 17:41:31 +0900 Subject: [PATCH 388/502] feat: Added the ability to verify ID tokens using the value of id_token_signing_alg_values_supported retrieved from DiscoveryEndpoint (#579) * feat(rp): to use signing algorithms from discovery configuration (#574) * feat: WithSigningAlgsFromDiscovery to verify IDTokenVerifier() behavior in RP with --- pkg/client/integration_test.go | 86 ++++++++++++++++++++++++++++++++++ pkg/client/rp/relying_party.go | 25 +++++++--- 2 files changed, 105 insertions(+), 6 deletions(-) diff --git a/pkg/client/integration_test.go b/pkg/client/integration_test.go index 9145c1e..98a9d3a 100644 --- a/pkg/client/integration_test.go +++ b/pkg/client/integration_test.go @@ -111,6 +111,92 @@ func testRelyingPartySession(t *testing.T, wrapServer bool) { } } +func TestRelyingPartyWithSigningAlgsFromDiscovery(t *testing.T) { + targetURL := "http://local-site" + localURL, err := url.Parse(targetURL + "/login?requestID=1234") + require.NoError(t, err, "local url") + + t.Log("------- start example OP ------") + seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano())) + clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25) + clientSecret := "secret" + client := storage.WebClient(clientID, clientSecret, targetURL) + storage.RegisterClients(client) + exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL)) + var dh deferredHandler + opServer := httptest.NewServer(&dh) + defer opServer.Close() + dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger, true) + + t.Log("------- create RP ------") + provider, err := rp.NewRelyingPartyOIDC( + CTX, + opServer.URL, + clientID, + clientSecret, + targetURL, + []string{"openid"}, + rp.WithSigningAlgsFromDiscovery(), + ) + require.NoError(t, err, "new rp") + + t.Log("------- run authorization code flow ------") + jar, err := cookiejar.New(nil) + require.NoError(t, err, "create cookie jar") + httpClient := &http.Client{ + Timeout: time.Second * 5, + CheckRedirect: func(_ *http.Request, _ []*http.Request) error { + return http.ErrUseLastResponse + }, + Jar: jar, + } + state := "state-" + strconv.FormatInt(seed.Int63(), 25) + capturedW := httptest.NewRecorder() + get := httptest.NewRequest("GET", localURL.String(), nil) + rp.AuthURLHandler(func() string { return state }, provider, + rp.WithPromptURLParam("Hello, World!", "Goodbye, World!"), + rp.WithURLParam("custom", "param"), + )(capturedW, get) + defer func() { + if t.Failed() { + t.Log("response body (redirect from RP to OP)", capturedW.Body.String()) + } + }() + resp := capturedW.Result() + startAuthURL, err := resp.Location() + require.NoError(t, err, "get redirect") + loginPageURL := getRedirect(t, "get redirect to login page", httpClient, startAuthURL) + form := getForm(t, "get login form", httpClient, loginPageURL) + defer func() { + if t.Failed() { + t.Logf("login form (unfilled): %s", string(form)) + } + }() + postLoginRedirectURL := fillForm(t, "fill login form", httpClient, form, loginPageURL, + gosubmit.Set("username", "test-user@local-site"), + gosubmit.Set("password", "verysecure"), + ) + codeBearingURL := getRedirect(t, "get redirect with code", httpClient, postLoginRedirectURL) + capturedW = httptest.NewRecorder() + get = httptest.NewRequest("GET", codeBearingURL.String(), nil) + var idToken string + redirect := func(w http.ResponseWriter, r *http.Request, newTokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) { + idToken = newTokens.IDToken + http.Redirect(w, r, targetURL, http.StatusFound) + } + rp.CodeExchangeHandler(rp.UserinfoCallback(redirect), provider)(capturedW, get) + defer func() { + if t.Failed() { + t.Log("token exchange response body", capturedW.Body.String()) + require.GreaterOrEqual(t, capturedW.Code, 200, "captured response code") + } + }() + + t.Log("------- verify id token ------") + _, err = rp.VerifyIDToken[*oidc.IDTokenClaims](CTX, idToken, provider.IDTokenVerifier()) + require.NoError(t, err, "verify id token") +} + func TestResourceServerTokenExchange(t *testing.T) { for _, wrapServer := range []bool{false, true} { t.Run(fmt.Sprint("wrapServer ", wrapServer), func(t *testing.T) { diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index bd2041b..ac7f466 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -90,12 +90,13 @@ var DefaultUnauthorizedHandler UnauthorizedHandler = func(w http.ResponseWriter, } type relyingParty struct { - issuer string - DiscoveryEndpoint string - endpoints Endpoints - oauthConfig *oauth2.Config - oauth2Only bool - pkce bool + issuer string + DiscoveryEndpoint string + endpoints Endpoints + oauthConfig *oauth2.Config + oauth2Only bool + pkce bool + useSigningAlgsFromDiscovery bool httpClient *http.Client cookieHandler *httphelper.CookieHandler @@ -238,6 +239,9 @@ func NewRelyingPartyOIDC(ctx context.Context, issuer, clientID, clientSecret, re if err != nil { return nil, err } + if rp.useSigningAlgsFromDiscovery { + rp.verifierOpts = append(rp.verifierOpts, WithSupportedSigningAlgorithms(discoveryConfiguration.IDTokenSigningAlgValuesSupported...)) + } endpoints := GetEndpoints(discoveryConfiguration) rp.oauthConfig.Endpoint = endpoints.Endpoint rp.endpoints = endpoints @@ -348,6 +352,15 @@ func WithLogger(logger *slog.Logger) Option { } } +// WithSigningAlgsFromDiscovery appends the [WithSupportedSigningAlgorithms] option to the Verifier Options. +// The algorithms returned in the `id_token_signing_alg_values_supported` from the discovery response will be set. +func WithSigningAlgsFromDiscovery() Option { + return func(rp *relyingParty) error { + rp.useSigningAlgsFromDiscovery = true + return nil + } +} + type SignerFromKey func() (jose.Signer, error) func SignerFromKeyPath(path string) SignerFromKey { From 79daaf1a7aa81f94c308a92995bcaa408fb167d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 20:27:12 +0300 Subject: [PATCH 389/502] chore(deps): bump golang.org/x/net from 0.22.0 to 0.23.0 (#592) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.22.0 to 0.23.0. - [Commits](https://github.com/golang/net/compare/v0.22.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 17efbc3..79c7b5b 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( go.opentelemetry.io/otel/metric v1.25.0 // indirect go.opentelemetry.io/otel/trace v1.25.0 // indirect golang.org/x/crypto v0.22.0 // indirect - golang.org/x/net v0.22.0 // indirect + golang.org/x/net v0.23.0 // indirect golang.org/x/sys v0.19.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0f48ad9..a933e2f 100644 --- a/go.sum +++ b/go.sum @@ -70,8 +70,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= From 3512c72f1c158e1c40157d84d50c97ecaa1d91d8 Mon Sep 17 00:00:00 2001 From: Kotaro Otaka <117251387+otakakot@users.noreply.github.com> Date: Mon, 22 Apr 2024 20:40:21 +0900 Subject: [PATCH 390/502] fix: to propagate context (#593) Co-authored-by: Livio Spring --- pkg/client/rp/jwks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/client/rp/jwks.go b/pkg/client/rp/jwks.go index 4a8c41b..c44a267 100644 --- a/pkg/client/rp/jwks.go +++ b/pkg/client/rp/jwks.go @@ -217,7 +217,7 @@ func (r *remoteKeySet) fetchRemoteKeys(ctx context.Context) ([]jose.JSONWebKey, ctx, span := client.Tracer.Start(ctx, "fetchRemoteKeys") defer span.End() - req, err := http.NewRequest("GET", r.jwksURL, nil) + req, err := http.NewRequestWithContext(ctx, "GET", r.jwksURL, nil) if err != nil { return nil, fmt.Errorf("oidc: can't create request: %v", err) } From 3e329dd0495b1e26aa5eda25969b096209db65f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Apr 2024 10:15:22 +0200 Subject: [PATCH 391/502] chore(deps): bump go.opentelemetry.io/otel from 1.25.0 to 1.26.0 (#595) Bumps [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) from 1.25.0 to 1.26.0. - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.25.0...v1.26.0) --- updated-dependencies: - dependency-name: go.opentelemetry.io/otel dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 79c7b5b..a715abb 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/stretchr/testify v1.9.0 github.com/zitadel/logging v0.6.0 github.com/zitadel/schema v1.3.0 - go.opentelemetry.io/otel v1.25.0 + go.opentelemetry.io/otel v1.26.0 golang.org/x/oauth2 v0.19.0 golang.org/x/text v0.14.0 ) @@ -29,8 +29,8 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel/metric v1.25.0 // indirect - go.opentelemetry.io/otel/trace v1.25.0 // indirect + go.opentelemetry.io/otel/metric v1.26.0 // indirect + go.opentelemetry.io/otel/trace v1.26.0 // indirect golang.org/x/crypto v0.22.0 // indirect golang.org/x/net v0.23.0 // indirect golang.org/x/sys v0.19.0 // indirect diff --git a/go.sum b/go.sum index a933e2f..aae5a55 100644 --- a/go.sum +++ b/go.sum @@ -54,12 +54,12 @@ github.com/zitadel/logging v0.6.0 h1:t5Nnt//r+m2ZhhoTmoPX+c96pbMarqJvW1Vq6xFTank github.com/zitadel/logging v0.6.0/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= -go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k= -go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg= -go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA= -go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s= -go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM= -go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I= +go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= +go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= +go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= +go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= +go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= +go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= From 099081fc1e4d455e56c3122b0f67fc6f76dde312 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Apr 2024 08:17:21 +0000 Subject: [PATCH 392/502] chore(deps): bump github.com/rs/cors from 1.10.1 to 1.11.0 (#596) Bumps [github.com/rs/cors](https://github.com/rs/cors) from 1.10.1 to 1.11.0. - [Commits](https://github.com/rs/cors/compare/v1.10.1...v1.11.0) --- updated-dependencies: - dependency-name: github.com/rs/cors dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a715abb..80e8933 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/jeremija/gosubmit v0.2.7 github.com/muhlemmer/gu v0.3.1 github.com/muhlemmer/httpforwarded v0.1.0 - github.com/rs/cors v1.10.1 + github.com/rs/cors v1.11.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.0 github.com/zitadel/logging v0.6.0 diff --git a/go.sum b/go.sum index aae5a55..2034402 100644 --- a/go.sum +++ b/go.sum @@ -41,8 +41,8 @@ github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/ github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= -github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= +github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From 37ca0e472a239c6623d083c3d05052fe0b576237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 30 Apr 2024 20:27:12 +0300 Subject: [PATCH 393/502] feat(op): authorize callback handler as argument in legacy server registration (#598) This change requires an additional argument to the op.RegisterLegacyServer constructor which passes the Authorize Callback Handler. This allows implementations to use their own handler instead of the one provided by the package. The current handler is exported for legacy behavior. This change is not considered breaking, as RegisterLegacyServer is flagged experimental. Related to https://github.com/zitadel/zitadel/issues/6882 --- example/server/exampleop/op.go | 2 +- pkg/op/auth_request.go | 2 +- pkg/op/op.go | 2 +- pkg/op/server_http_routes_test.go | 2 +- pkg/op/server_legacy.go | 11 +++++------ 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/example/server/exampleop/op.go b/example/server/exampleop/op.go index e502536..e8ef892 100644 --- a/example/server/exampleop/op.go +++ b/example/server/exampleop/op.go @@ -80,7 +80,7 @@ func SetupServer(issuer string, storage Storage, logger *slog.Logger, wrapServer handler := http.Handler(provider) if wrapServer { - handler = op.RegisterLegacyServer(op.NewLegacyServer(provider, *op.DefaultEndpoints)) + handler = op.RegisterLegacyServer(op.NewLegacyServer(provider, *op.DefaultEndpoints), op.AuthorizeCallbackHandler(provider)) } // we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration) diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 4b5837a..923b9a7 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -61,7 +61,7 @@ func authorizeHandler(authorizer Authorizer) func(http.ResponseWriter, *http.Req } } -func authorizeCallbackHandler(authorizer Authorizer) func(http.ResponseWriter, *http.Request) { +func AuthorizeCallbackHandler(authorizer Authorizer) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { AuthorizeCallback(w, r, authorizer) } diff --git a/pkg/op/op.go b/pkg/op/op.go index 9fd6b30..61c2449 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -135,7 +135,7 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) chi.Router router.HandleFunc(readinessEndpoint, readyHandler(o.Probes())) router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Storage())) router.HandleFunc(o.AuthorizationEndpoint().Relative(), authorizeHandler(o)) - router.HandleFunc(authCallbackPath(o), authorizeCallbackHandler(o)) + router.HandleFunc(authCallbackPath(o), AuthorizeCallbackHandler(o)) router.HandleFunc(o.TokenEndpoint().Relative(), tokenHandler(o)) router.HandleFunc(o.IntrospectionEndpoint().Relative(), introspectionHandler(o)) router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o)) diff --git a/pkg/op/server_http_routes_test.go b/pkg/op/server_http_routes_test.go index 8b3fa02..2c83ad3 100644 --- a/pkg/op/server_http_routes_test.go +++ b/pkg/op/server_http_routes_test.go @@ -32,7 +32,7 @@ func jwtProfile() (string, error) { } func TestServerRoutes(t *testing.T) { - server := op.RegisterLegacyServer(op.NewLegacyServer(testProvider, *op.DefaultEndpoints)) + server := op.RegisterLegacyServer(op.NewLegacyServer(testProvider, *op.DefaultEndpoints), op.AuthorizeCallbackHandler(testProvider)) storage := testProvider.Storage().(routesTestStorage) ctx := op.ContextWithIssuer(context.Background(), testIssuer) diff --git a/pkg/op/server_legacy.go b/pkg/op/server_legacy.go index 6b6d4b3..126fde1 100644 --- a/pkg/op/server_legacy.go +++ b/pkg/op/server_legacy.go @@ -22,17 +22,16 @@ type ExtendedLegacyServer interface { } // RegisterLegacyServer registers a [LegacyServer] or an extension thereof. -// It takes care of registering the IssuerFromRequest middleware -// and Authorization Callback Routes. +// It takes care of registering the IssuerFromRequest middleware. +// The authorizeCallbackHandler is registered on `/callback` under the authorization endpoint. // Neither are part of the bare [Server] interface. // // EXPERIMENTAL: may change until v4 -func RegisterLegacyServer(s ExtendedLegacyServer, options ...ServerOption) http.Handler { - provider := s.Provider() +func RegisterLegacyServer(s ExtendedLegacyServer, authorizeCallbackHandler http.HandlerFunc, options ...ServerOption) http.Handler { options = append(options, - WithHTTPMiddleware(intercept(provider.IssuerFromRequest)), + WithHTTPMiddleware(intercept(s.Provider().IssuerFromRequest)), WithSetRouter(func(r chi.Router) { - r.HandleFunc(s.Endpoints().Authorization.Relative()+authCallbackPathSuffix, authorizeCallbackHandler(provider)) + r.HandleFunc(s.Endpoints().Authorization.Relative()+authCallbackPathSuffix, authorizeCallbackHandler) }), ) return RegisterServer(s, s.Endpoints(), options...) From 24d43f538ebf85b8eb1884b5dac5fbec82a9c103 Mon Sep 17 00:00:00 2001 From: Yuval Marcus Date: Thu, 2 May 2024 03:46:12 -0400 Subject: [PATCH 394/502] fix: Handle case where verifier Nonce func is nil (#594) * Skip nonce check if verifier nonce func is nil * add unit test --- pkg/client/rp/verifier.go | 6 ++-- pkg/client/rp/verifier_test.go | 60 +++++++++++++++++++--------------- 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/pkg/client/rp/verifier.go b/pkg/client/rp/verifier.go index 5a07d8a..ca59454 100644 --- a/pkg/client/rp/verifier.go +++ b/pkg/client/rp/verifier.go @@ -73,8 +73,10 @@ func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v *IDTokenV return nilClaims, err } - if err = oidc.CheckNonce(claims, v.Nonce(ctx)); err != nil { - return nilClaims, err + if v.Nonce != nil { + if err = oidc.CheckNonce(claims, v.Nonce(ctx)); err != nil { + return nilClaims, err + } } if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR); err != nil { diff --git a/pkg/client/rp/verifier_test.go b/pkg/client/rp/verifier_test.go index cd2fab4..24d35af 100644 --- a/pkg/client/rp/verifier_test.go +++ b/pkg/client/rp/verifier_test.go @@ -100,22 +100,21 @@ func TestVerifyIDToken(t *testing.T) { MaxAge: 2 * time.Minute, ACR: tu.ACRVerify, Nonce: func(context.Context) string { return tu.ValidNonce }, + ClientID: tu.ValidClientID, } tests := []struct { - name string - clientID string - tokenClaims func() (string, *oidc.IDTokenClaims) - wantErr bool + name string + tokenClaims func() (string, *oidc.IDTokenClaims) + customVerifier func(verifier *IDTokenVerifier) + wantErr bool }{ { name: "success", - clientID: tu.ValidClientID, tokenClaims: tu.ValidIDToken, }, { - name: "custom claims", - clientID: tu.ValidClientID, + name: "custom claims", tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.NewIDTokenCustom( tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, @@ -125,21 +124,31 @@ func TestVerifyIDToken(t *testing.T) { ) }, }, + { + name: "skip nonce check", + customVerifier: func(verifier *IDTokenVerifier) { + verifier.Nonce = nil + }, + tokenClaims: func() (string, *oidc.IDTokenClaims) { + return tu.NewIDToken( + tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, + tu.ValidExpiration, tu.ValidAuthTime, "foo", + tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "", + ) + }, + }, { name: "parse err", - clientID: tu.ValidClientID, tokenClaims: func() (string, *oidc.IDTokenClaims) { return "~~~~", nil }, wantErr: true, }, { name: "invalid signature", - clientID: tu.ValidClientID, tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.InvalidSignatureToken, nil }, wantErr: true, }, { - name: "empty subject", - clientID: tu.ValidClientID, + name: "empty subject", tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.NewIDToken( tu.ValidIssuer, "", tu.ValidAudience, @@ -150,8 +159,7 @@ func TestVerifyIDToken(t *testing.T) { wantErr: true, }, { - name: "wrong issuer", - clientID: tu.ValidClientID, + name: "wrong issuer", tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.NewIDToken( "foo", tu.ValidSubject, tu.ValidAudience, @@ -162,14 +170,15 @@ func TestVerifyIDToken(t *testing.T) { wantErr: true, }, { - name: "wrong clientID", - clientID: "foo", + name: "wrong clientID", + customVerifier: func(verifier *IDTokenVerifier) { + verifier.ClientID = "foo" + }, tokenClaims: tu.ValidIDToken, wantErr: true, }, { - name: "expired", - clientID: tu.ValidClientID, + name: "expired", tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.NewIDToken( tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, @@ -180,8 +189,7 @@ func TestVerifyIDToken(t *testing.T) { wantErr: true, }, { - name: "wrong IAT", - clientID: tu.ValidClientID, + name: "wrong IAT", tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.NewIDToken( tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, @@ -192,8 +200,7 @@ func TestVerifyIDToken(t *testing.T) { wantErr: true, }, { - name: "wrong acr", - clientID: tu.ValidClientID, + name: "wrong acr", tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.NewIDToken( tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, @@ -204,8 +211,7 @@ func TestVerifyIDToken(t *testing.T) { wantErr: true, }, { - name: "expired auth", - clientID: tu.ValidClientID, + name: "expired auth", tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.NewIDToken( tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, @@ -216,8 +222,7 @@ func TestVerifyIDToken(t *testing.T) { wantErr: true, }, { - name: "wrong nonce", - clientID: tu.ValidClientID, + name: "wrong nonce", tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.NewIDToken( tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, @@ -231,7 +236,10 @@ func TestVerifyIDToken(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { token, want := tt.tokenClaims() - verifier.ClientID = tt.clientID + if tt.customVerifier != nil { + tt.customVerifier(verifier) + } + got, err := VerifyIDToken[*oidc.IDTokenClaims](context.Background(), token, verifier) if tt.wantErr { assert.Error(t, err) From 5a84d8c4bcf9236616081224f15f91eaae2d4538 Mon Sep 17 00:00:00 2001 From: Yuval Marcus Date: Mon, 6 May 2024 02:13:52 -0400 Subject: [PATCH 395/502] fix: Omit non-standard, empty fields in `RefreshTokenRequest` when performing a token refresh (#599) * Add omitempty tags * Add omitempty to more fields --- pkg/client/rp/relying_party.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index ac7f466..029a897 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -734,11 +734,11 @@ func (t tokenEndpointCaller) TokenEndpoint() string { type RefreshTokenRequest struct { RefreshToken string `schema:"refresh_token"` - Scopes oidc.SpaceDelimitedArray `schema:"scope"` - ClientID string `schema:"client_id"` - ClientSecret string `schema:"client_secret"` - ClientAssertion string `schema:"client_assertion"` - ClientAssertionType string `schema:"client_assertion_type"` + Scopes oidc.SpaceDelimitedArray `schema:"scope,omitempty"` + ClientID string `schema:"client_id,omitempty"` + ClientSecret string `schema:"client_secret,omitempty"` + ClientAssertion string `schema:"client_assertion,omitempty"` + ClientAssertionType string `schema:"client_assertion_type,omitempty"` GrantType oidc.GrantType `schema:"grant_type"` } From 30184ae0545c571a9f3b5537cacb8e60e888ac6b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 06:41:53 +0000 Subject: [PATCH 396/502] chore(deps): bump golang.org/x/text from 0.14.0 to 0.15.0 (#600) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.14.0 to 0.15.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.14.0...v0.15.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Livio Spring --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 80e8933..4416887 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.26.0 golang.org/x/oauth2 v0.19.0 - golang.org/x/text v0.14.0 + golang.org/x/text v0.15.0 ) require ( diff --git a/go.sum b/go.sum index 2034402..4e3024a 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,8 @@ golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= From 20d0f189a85fe2bb00cdaaaa737333ea8b728213 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 06:44:21 +0000 Subject: [PATCH 397/502] chore(deps): bump golang.org/x/oauth2 from 0.19.0 to 0.20.0 (#601) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.19.0 to 0.20.0. - [Commits](https://github.com/golang/oauth2/compare/v0.19.0...v0.20.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4416887..7cf8a68 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/zitadel/logging v0.6.0 github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.26.0 - golang.org/x/oauth2 v0.19.0 + golang.org/x/oauth2 v0.20.0 golang.org/x/text v0.15.0 ) diff --git a/go.sum b/go.sum index 4e3024a..14be2fc 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= -golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= +golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= +golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= From 6d1231cb37c27f29ed6fc52bb52d5e7e260ca2dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 May 2024 07:58:56 +0200 Subject: [PATCH 398/502] chore(deps): bump codecov/codecov-action from 4.3.0 to 4.3.1 (#604) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.3.0 to 4.3.1. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4.3.0...v4.3.1) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a6d3826..e149a84 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v4.3.0 + - uses: codecov/codecov-action@v4.3.1 with: file: ./profile.cov name: codecov-go From 7437309a42c70191dfdf63ab8713d1c8048e0a63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 May 2024 10:56:21 +0200 Subject: [PATCH 399/502] chore(deps): bump github.com/go-jose/go-jose/v4 from 4.0.1 to 4.0.2 (#608) Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.0.1 to 4.0.2. - [Release notes](https://github.com/go-jose/go-jose/releases) - [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md) - [Commits](https://github.com/go-jose/go-jose/compare/v4.0.1...v4.0.2) --- updated-dependencies: - dependency-name: github.com/go-jose/go-jose/v4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7cf8a68..e00f653 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 require ( github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/go-chi/chi/v5 v5.0.12 - github.com/go-jose/go-jose/v4 v4.0.1 + github.com/go-jose/go-jose/v4 v4.0.2 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index 14be2fc..fa16701 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ 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/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= -github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= +github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= From 8a47532a8efa40edb765881ab128ce0a233c3af6 Mon Sep 17 00:00:00 2001 From: minami yoshihiko Date: Fri, 17 May 2024 19:17:54 +0900 Subject: [PATCH 400/502] feat: add default signature algorithms (#606) --- pkg/oidc/verifier.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go index 410b383..cb66676 100644 --- a/pkg/oidc/verifier.go +++ b/pkg/oidc/verifier.go @@ -186,7 +186,7 @@ func toJoseSignatureAlgorithms(algorithms []string) []jose.SignatureAlgorithm { out[i] = jose.SignatureAlgorithm(algorithms[i]) } if len(out) == 0 { - out = append(out, jose.RS256) + out = append(out, jose.RS256, jose.ES256, jose.PS256) } return out } From 7714a3b1137af89f87cae691189406600eddc785 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 12:56:32 +0200 Subject: [PATCH 401/502] --- (#609) updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e149a84..8beba66 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v4.3.1 + - uses: codecov/codecov-action@v4.4.1 with: file: ./profile.cov name: codecov-go From 7037344cf40cbc781532bca0f2eaa80aca8a6909 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 10:23:36 +0200 Subject: [PATCH 402/502] --- (#610) updated-dependencies: - dependency-name: go.opentelemetry.io/otel dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index e00f653..6cd07aa 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/stretchr/testify v1.9.0 github.com/zitadel/logging v0.6.0 github.com/zitadel/schema v1.3.0 - go.opentelemetry.io/otel v1.26.0 + go.opentelemetry.io/otel v1.27.0 golang.org/x/oauth2 v0.20.0 golang.org/x/text v0.15.0 ) @@ -29,8 +29,8 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel/metric v1.26.0 // indirect - go.opentelemetry.io/otel/trace v1.26.0 // indirect + go.opentelemetry.io/otel/metric v1.27.0 // indirect + go.opentelemetry.io/otel/trace v1.27.0 // indirect golang.org/x/crypto v0.22.0 // indirect golang.org/x/net v0.23.0 // indirect golang.org/x/sys v0.19.0 // indirect diff --git a/go.sum b/go.sum index fa16701..e70bbcf 100644 --- a/go.sum +++ b/go.sum @@ -54,12 +54,12 @@ github.com/zitadel/logging v0.6.0 h1:t5Nnt//r+m2ZhhoTmoPX+c96pbMarqJvW1Vq6xFTank github.com/zitadel/logging v0.6.0/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= -go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= -go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= -go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= -go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= -go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= -go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= +go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= +go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= +go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= +go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= +go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= From 7a8f8ade4dd93239d9f697c3ac4d3d95dfa306de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:14:04 +0200 Subject: [PATCH 403/502] chore(deps): bump golang.org/x/text from 0.15.0 to 0.16.0 (#612) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.15.0 to 0.16.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.15.0...v0.16.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6cd07aa..6ad9cb0 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.27.0 golang.org/x/oauth2 v0.20.0 - golang.org/x/text v0.15.0 + golang.org/x/text v0.16.0 ) require ( diff --git a/go.sum b/go.sum index e70bbcf..f81cd1b 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,8 @@ golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= From 9ecdd0cf9a73bf339b737a6459b07aa0a251c603 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Jun 2024 08:16:06 +0000 Subject: [PATCH 404/502] chore(deps): bump golang.org/x/oauth2 from 0.20.0 to 0.21.0 (#611) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.20.0 to 0.21.0. - [Commits](https://github.com/golang/oauth2/compare/v0.20.0...v0.21.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6ad9cb0..4f180c6 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/zitadel/logging v0.6.0 github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.27.0 - golang.org/x/oauth2 v0.20.0 + golang.org/x/oauth2 v0.21.0 golang.org/x/text v0.16.0 ) diff --git a/go.sum b/go.sum index f81cd1b..4cb8219 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= From a7b53555808637136ce591513282b7c9dae63131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Thu, 13 Jun 2024 08:16:46 +0200 Subject: [PATCH 405/502] feat(op): allow scope without openid (#613) This changes removes the requirement of the openid scope to be set for all token requests. As this library also support OAuth2-only authentication mechanisms we still want to sanitize requested scopes, but not enforce the openid scope. Related to https://github.com/zitadel/zitadel/discussions/8068 --- pkg/op/auth_request.go | 29 ++++++++--------------------- pkg/op/auth_request_test.go | 15 --------------- 2 files changed, 8 insertions(+), 36 deletions(-) diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 923b9a7..fe73180 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -11,6 +11,7 @@ import ( "net" "net/http" "net/url" + "slices" "strings" "time" @@ -250,37 +251,23 @@ func ValidateAuthReqPrompt(prompts []string, maxAge *uint) (_ *uint, err error) return maxAge, nil } -// ValidateAuthReqScopes validates the passed scopes +// ValidateAuthReqScopes validates the passed scopes and deletes any unsupported scopes. +// An error is returned if scopes is empty. func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) { if len(scopes) == 0 { return nil, oidc.ErrInvalidRequest(). WithDescription("The scope of your request is missing. Please ensure some scopes are requested. " + "If you have any questions, you may contact the administrator of the application.") } - openID := false - for i := len(scopes) - 1; i >= 0; i-- { - scope := scopes[i] - if scope == oidc.ScopeOpenID { - openID = true - continue - } - if !(scope == oidc.ScopeProfile || + scopes = slices.DeleteFunc(scopes, func(scope string) bool { + return !(scope == oidc.ScopeOpenID || + scope == oidc.ScopeProfile || scope == oidc.ScopeEmail || scope == oidc.ScopePhone || scope == oidc.ScopeAddress || scope == oidc.ScopeOfflineAccess) && - !client.IsScopeAllowed(scope) { - scopes[i] = scopes[len(scopes)-1] - scopes[len(scopes)-1] = "" - scopes = scopes[:len(scopes)-1] - } - } - if !openID { - return nil, oidc.ErrInvalidScope().WithDescription("The scope openid is missing in your request. " + - "Please ensure the scope openid is added to the request. " + - "If you have any questions, you may contact the administrator of the application.") - } - + !client.IsScopeAllowed(scope) + }) return scopes, nil } diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index 45627a5..6b4af17 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -137,11 +137,6 @@ func TestValidateAuthRequest(t *testing.T) { args{&oidc.AuthRequest{}, mock.NewMockStorageExpectValidClientID(t), nil}, oidc.ErrInvalidRequest(), }, - { - "scope openid missing fails", - args{&oidc.AuthRequest{Scopes: []string{"profile"}}, mock.NewMockStorageExpectValidClientID(t), nil}, - oidc.ErrInvalidScope(), - }, { "response_type missing fails", args{&oidc.AuthRequest{Scopes: []string{"openid"}}, mock.NewMockStorageExpectValidClientID(t), nil}, @@ -287,16 +282,6 @@ func TestValidateAuthReqScopes(t *testing.T) { err: true, }, }, - { - "scope openid missing fails", - args{ - mock.NewClientExpectAny(t, op.ApplicationTypeWeb), - []string{"email"}, - }, - res{ - err: true, - }, - }, { "scope ok", args{ From da4e683bd3a2d08206d4e7dd0e34423fec5e7419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 14 Jun 2024 07:40:05 +0200 Subject: [PATCH 406/502] fix(example): set content-type in the userinfo response (#614) This change sets the `content-type` header to `application/json` for the response sent to the browser in the app example. This enables pretty-printing of the userinfo json document in at least Chromium. --- example/client/app/app.go | 1 + 1 file changed, 1 insertion(+) diff --git a/example/client/app/app.go b/example/client/app/app.go index 99aba3d..448c530 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -108,6 +108,7 @@ func main() { http.Error(w, err.Error(), http.StatusInternalServerError) return } + w.Header().Set("content-type", "application/json") w.Write(data) } From 1c2dc2c0e1a8a0f20f8923363d4ddb5b35213ecf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Jun 2024 07:31:39 +0200 Subject: [PATCH 407/502] chore(deps): bump codecov/codecov-action from 4.4.1 to 4.5.0 (#615) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.4.1 to 4.5.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4.4.1...v4.5.0) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8beba66..48690cf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v4.4.1 + - uses: codecov/codecov-action@v4.5.0 with: file: ./profile.cov name: codecov-go From 371a5aaab4dd2a5e140e7b242553e72cbe5087fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 22 Jun 2024 10:08:01 +0200 Subject: [PATCH 408/502] chore(deps): bump github.com/go-chi/chi/v5 from 5.0.12 to 5.0.13 (#616) Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.0.12 to 5.0.13. - [Release notes](https://github.com/go-chi/chi/releases) - [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md) - [Commits](https://github.com/go-chi/chi/compare/v5.0.12...v5.0.13) --- updated-dependencies: - dependency-name: github.com/go-chi/chi/v5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4f180c6..f85fa14 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/bmatcuk/doublestar/v4 v4.6.1 - github.com/go-chi/chi/v5 v5.0.12 + github.com/go-chi/chi/v5 v5.0.13 github.com/go-jose/go-jose/v4 v4.0.2 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 diff --git a/go.sum b/go.sum index 4cb8219..1e6c057 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTS 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/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= -github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.0.13 h1:JlH2F2M8qnwl0N1+JFFzlX9TlKJYas3aPXdiuTmJL+w= +github.com/go-chi/chi/v5 v5.0.13/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= From a09d9f7390c05ed5e3a63e4f84b533078a015ffb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 10:42:22 +0200 Subject: [PATCH 409/502] chore(deps): bump github.com/go-chi/chi/v5 from 5.0.13 to 5.0.14 (#617) Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.0.13 to 5.0.14. - [Release notes](https://github.com/go-chi/chi/releases) - [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md) - [Commits](https://github.com/go-chi/chi/compare/v5.0.13...v5.0.14) --- updated-dependencies: - dependency-name: github.com/go-chi/chi/v5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f85fa14..682ba30 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/bmatcuk/doublestar/v4 v4.6.1 - github.com/go-chi/chi/v5 v5.0.13 + github.com/go-chi/chi/v5 v5.0.14 github.com/go-jose/go-jose/v4 v4.0.2 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 diff --git a/go.sum b/go.sum index 1e6c057..a6011da 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTS 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/go-chi/chi/v5 v5.0.13 h1:JlH2F2M8qnwl0N1+JFFzlX9TlKJYas3aPXdiuTmJL+w= -github.com/go-chi/chi/v5 v5.0.13/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0= +github.com/go-chi/chi/v5 v5.0.14/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= From 954802b63b38599ae3d652b1e2a7b20f7a3224ab Mon Sep 17 00:00:00 2001 From: dkaminer Date: Thu, 27 Jun 2024 12:05:47 +0300 Subject: [PATCH 410/502] Updating indirect dependencies version in the OIDC GitHub library (#618) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit golang.org/x/crypto, Version: v0.22.0 -→ v0.24.0 golang.org/x/net, Version: v0.23.0 -→ v0.26.0 golang.org/x/sys, Version: v0.19.0 -→ v0.21.0 Co-authored-by: Daphna Kaminer --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 682ba30..da1adb1 100644 --- a/go.mod +++ b/go.mod @@ -31,8 +31,8 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/otel/metric v1.27.0 // indirect go.opentelemetry.io/otel/trace v1.27.0 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/net v0.23.0 // indirect - golang.org/x/sys v0.19.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sys v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a6011da..6db9376 100644 --- a/go.sum +++ b/go.sum @@ -62,16 +62,16 @@ go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5 go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= @@ -83,8 +83,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From e87f433e099afabc8b10f5684ad142fc3d8b4a0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 12:20:28 +0300 Subject: [PATCH 411/502] chore(deps): bump github.com/go-chi/chi/v5 from 5.0.14 to 5.1.0 (#619) Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.0.14 to 5.1.0. - [Release notes](https://github.com/go-chi/chi/releases) - [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md) - [Commits](https://github.com/go-chi/chi/compare/v5.0.14...v5.1.0) --- updated-dependencies: - dependency-name: github.com/go-chi/chi/v5 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index da1adb1..0bce56f 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/bmatcuk/doublestar/v4 v4.6.1 - github.com/go-chi/chi/v5 v5.0.14 + github.com/go-chi/chi/v5 v5.1.0 github.com/go-jose/go-jose/v4 v4.0.2 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 diff --git a/go.sum b/go.sum index 6db9376..19894c4 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTS 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/go-chi/chi/v5 v5.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0= -github.com/go-chi/chi/v5 v5.0.14/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= From d6b4dc6b2f673b07a06e8fde3ee75ce1886f364e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jul 2024 09:20:44 +0200 Subject: [PATCH 412/502] chore(deps): bump actions/add-to-project from 1.0.1 to 1.0.2 (#620) Bumps [actions/add-to-project](https://github.com/actions/add-to-project) from 1.0.1 to 1.0.2. - [Release notes](https://github.com/actions/add-to-project/releases) - [Commits](https://github.com/actions/add-to-project/compare/v1.0.1...v1.0.2) --- updated-dependencies: - dependency-name: actions/add-to-project dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/issue.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 5b1febf..480c339 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: add issue - uses: actions/add-to-project@v1.0.1 + uses: actions/add-to-project@v1.0.2 if: ${{ github.event_name == 'issues' }} with: # You can target a repository in a different organization @@ -28,7 +28,7 @@ jobs: username: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_PAT }} - name: add pr - uses: actions/add-to-project@v1.0.1 + uses: actions/add-to-project@v1.0.2 if: ${{ github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'engineers')}} with: # You can target a repository in a different organization From fc6716bf22416fc339ce4ae254bbadf92739c0f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Jul 2024 08:32:12 +0200 Subject: [PATCH 413/502] chore(deps): bump go.opentelemetry.io/otel from 1.27.0 to 1.28.0 (#622) Bumps [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) from 1.27.0 to 1.28.0. - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.27.0...v1.28.0) --- updated-dependencies: - dependency-name: go.opentelemetry.io/otel dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 0bce56f..80906a4 100644 --- a/go.mod +++ b/go.mod @@ -18,19 +18,19 @@ require ( github.com/stretchr/testify v1.9.0 github.com/zitadel/logging v0.6.0 github.com/zitadel/schema v1.3.0 - go.opentelemetry.io/otel v1.27.0 + go.opentelemetry.io/otel v1.28.0 golang.org/x/oauth2 v0.21.0 golang.org/x/text v0.16.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel/metric v1.27.0 // indirect - go.opentelemetry.io/otel/trace v1.27.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect golang.org/x/crypto v0.24.0 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/sys v0.21.0 // indirect diff --git a/go.sum b/go.sum index 19894c4..280feb7 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITL github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= @@ -54,12 +54,12 @@ github.com/zitadel/logging v0.6.0 h1:t5Nnt//r+m2ZhhoTmoPX+c96pbMarqJvW1Vq6xFTank github.com/zitadel/logging v0.6.0/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= -go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= -go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= -go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= -go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= -go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= -go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= From e5a428d4be261ce6fac5bbd1183698c170289b36 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Tue, 9 Jul 2024 15:55:50 +0200 Subject: [PATCH 414/502] feat: support PKCS#8 (#623) --- pkg/client/client.go | 9 +-- pkg/crypto/key.go | 35 ++++++++++-- pkg/crypto/key_test.go | 122 ++++++++++++++++++++++++++++++++--------- pkg/oidc/token.go | 5 +- 4 files changed, 134 insertions(+), 37 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index e17c70a..990da9b 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -12,11 +12,12 @@ import ( "github.com/go-jose/go-jose/v4" "github.com/zitadel/logging" + "go.opentelemetry.io/otel" + "golang.org/x/oauth2" + "github.com/zitadel/oidc/v3/pkg/crypto" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" - "go.opentelemetry.io/otel" - "golang.org/x/oauth2" ) var ( @@ -196,12 +197,12 @@ func CallTokenExchangeEndpoint(ctx context.Context, request any, authFn any, cal } func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) { - privateKey, err := crypto.BytesToPrivateKey(key) + privateKey, algorithm, err := crypto.BytesToPrivateKey(key) if err != nil { return nil, err } signingKey := jose.SigningKey{ - Algorithm: jose.RS256, + Algorithm: algorithm, Key: &jose.JSONWebKey{Key: privateKey, KeyID: keyID}, } return jose.NewSigner(signingKey, &jose.SignerOptions{}) diff --git a/pkg/crypto/key.go b/pkg/crypto/key.go index 79e2046..12bca28 100644 --- a/pkg/crypto/key.go +++ b/pkg/crypto/key.go @@ -1,22 +1,45 @@ package crypto import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" "crypto/rsa" "crypto/x509" "encoding/pem" "errors" + + "github.com/go-jose/go-jose/v4" ) -func BytesToPrivateKey(b []byte) (*rsa.PrivateKey, error) { +var ( + ErrPEMDecode = errors.New("PEM decode failed") + ErrUnsupportedFormat = errors.New("key is neither in PKCS#1 nor PKCS#8 format") + ErrUnsupportedPrivateKey = errors.New("unsupported key type, must be RSA, ECDSA or ED25519 private key") +) + +func BytesToPrivateKey(b []byte) (crypto.PublicKey, jose.SignatureAlgorithm, error) { block, _ := pem.Decode(b) if block == nil { - return nil, errors.New("PEM decode failed") + return nil, "", ErrPEMDecode } - key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err == nil { + return privateKey, jose.RS256, nil + } + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { - return nil, err + return nil, "", ErrUnsupportedFormat + } + switch privateKey := key.(type) { + case *rsa.PrivateKey: + return privateKey, jose.RS256, nil + case ed25519.PrivateKey: + return privateKey, jose.EdDSA, nil + case *ecdsa.PrivateKey: + return privateKey, jose.ES256, nil + default: + return nil, "", ErrUnsupportedPrivateKey } - - return key, nil } diff --git a/pkg/crypto/key_test.go b/pkg/crypto/key_test.go index 23ebdc0..8ed5cb5 100644 --- a/pkg/crypto/key_test.go +++ b/pkg/crypto/key_test.go @@ -1,21 +1,64 @@ package crypto_test import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" "testing" + "github.com/go-jose/go-jose/v4" "github.com/stretchr/testify/assert" - "github.com/zitadel/oidc/v3/pkg/crypto" + zcrypto "github.com/zitadel/oidc/v3/pkg/crypto" ) -func TestBytesToPrivateKey(tt *testing.T) { - tt.Run("PEMDecodeError", func(t *testing.T) { - _, err := crypto.BytesToPrivateKey([]byte("The non-PEM sequence")) - assert.EqualError(t, err, "PEM decode failed") - }) - - tt.Run("InvalidKeyFormat", func(t *testing.T) { - _, err := crypto.BytesToPrivateKey([]byte(`-----BEGIN PRIVATE KEY----- +func TestBytesToPrivateKey(t *testing.T) { + type args struct { + key []byte + } + type want struct { + key crypto.Signer + algorithm jose.SignatureAlgorithm + err error + } + tests := []struct { + name string + args args + want want + }{ + { + name: "PEMDecodeError", + args: args{ + key: []byte("The non-PEM sequence"), + }, + want: want{ + err: zcrypto.ErrPEMDecode, + }, + }, + { + name: "PKCS#1 RSA", + args: args{ + key: []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIBOgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu +KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm +o3qGy0t6z09AIJtH+5OeRV1be+N4cDYJKffGzDa88vQENZiRm0GRq6a+HPGQMd2k +TQIhAKMSvzIBnni7ot/OSie2TmJLY4SwTQAevXysE2RbFDYdAiEBCUEaRQnMnbp7 +9mxDXDf6AU0cN/RPBjb9qSHDcWZHGzUCIG2Es59z8ugGrDY+pxLQnwfotadxd+Uy +v/Ow5T0q5gIJAiEAyS4RaI9YG8EWx/2w0T67ZUVAw8eOMB6BIUg0Xcu+3okCIBOs +/5OiPgoTdSy7bcF9IGpSE8ZgGKzgYQVZeN97YE00 +-----END RSA PRIVATE KEY-----`), + }, + want: want{ + key: &rsa.PrivateKey{}, + algorithm: jose.RS256, + err: nil, + }, + }, + { + name: "PKCS#8 RSA", + args: args{ + key: []byte(`-----BEGIN PRIVATE KEY----- MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfaDB7pK/fmP/I 7IusSK8lTCBnPZghqIbVLt2QHYAMoEF1CaF4F4rxo2vl1Mt8gwsq4T3osQFZMvnL YHb7KNyUoJgTjLxJQADv2u4Q3U38heAzK5Tp4ry4MCnuyJIqAPK1GiruwEq4zQrx @@ -42,21 +85,50 @@ srJnjF0H8oKmAY6hw+1Tm/n/b08p+RyL48TgVSE2vhUCgYA3BWpkD4PlCcn/FZsq OrLFyFXI6jIaxskFtsRW1IxxIlAdZmxfB26P/2gx6VjLdxJI/RRPkJyEN2dP7CbR BDjb565dy1O9D6+UrY70Iuwjz+OcALRBBGTaiF2pLn6IhSzNI2sy/tXX8q8dBlg9 OFCrqT/emes3KytTPfa5NZtYeQ== ------END PRIVATE KEY-----`)) - assert.EqualError(t, err, "x509: failed to parse private key (use ParsePKCS8PrivateKey instead for this key format)") - }) +-----END PRIVATE KEY-----`), + }, + want: want{ + key: &rsa.PrivateKey{}, + algorithm: jose.RS256, + err: nil, + }, + }, + { + name: "PKCS#8 ECDSA", + args: args{ + key: []byte(`-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgwwOZSU4GlP7ps/Wp +V6o0qRwxultdfYo/uUuj48QZjSuhRANCAATMiI2Han+ABKmrk5CNlxRAGC61w4d3 +G4TAeuBpyzqJ7x/6NjCxoQzJzZHtNjIfjVATI59XFZWF59GhtSZbShAr +-----END PRIVATE KEY-----`), + }, + want: want{ + key: &ecdsa.PrivateKey{}, + algorithm: jose.ES256, + err: nil, + }, + }, + { + name: "PKCS#8 ED25519", + args: args{ + key: []byte(`-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIHu6ZtDsjjauMasBxnS9Fg87UJwKfcT/oiq6S0ktbky8 +-----END PRIVATE KEY-----`), + }, + want: want{ + key: ed25519.PrivateKey{}, + algorithm: jose.EdDSA, + err: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + key, algorithm, err := zcrypto.BytesToPrivateKey(tt.args.key) + assert.IsType(t, tt.want.key, key) + assert.Equal(t, tt.want.algorithm, algorithm) + assert.ErrorIs(t, tt.want.err, err) + }) - tt.Run("Ok", func(t *testing.T) { - key, err := crypto.BytesToPrivateKey([]byte(`-----BEGIN RSA PRIVATE KEY----- -MIIBOgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu -KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm -o3qGy0t6z09AIJtH+5OeRV1be+N4cDYJKffGzDa88vQENZiRm0GRq6a+HPGQMd2k -TQIhAKMSvzIBnni7ot/OSie2TmJLY4SwTQAevXysE2RbFDYdAiEBCUEaRQnMnbp7 -9mxDXDf6AU0cN/RPBjb9qSHDcWZHGzUCIG2Es59z8ugGrDY+pxLQnwfotadxd+Uy -v/Ow5T0q5gIJAiEAyS4RaI9YG8EWx/2w0T67ZUVAw8eOMB6BIUg0Xcu+3okCIBOs -/5OiPgoTdSy7bcF9IGpSE8ZgGKzgYQVZeN97YE00 ------END RSA PRIVATE KEY-----`)) - assert.NoError(t, err) - assert.NotNil(t, key) - }) + } } diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index 8d2880c..5b18dac 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -9,6 +9,7 @@ import ( "golang.org/x/oauth2" "github.com/muhlemmer/gu" + "github.com/zitadel/oidc/v3/pkg/crypto" ) @@ -344,12 +345,12 @@ func AppendClientIDToAudience(clientID string, audience []string) []string { } func GenerateJWTProfileToken(assertion *JWTProfileAssertionClaims) (string, error) { - privateKey, err := crypto.BytesToPrivateKey(assertion.PrivateKey) + privateKey, algorithm, err := crypto.BytesToPrivateKey(assertion.PrivateKey) if err != nil { return "", err } key := jose.SigningKey{ - Algorithm: jose.RS256, + Algorithm: algorithm, Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID}, } signer, err := jose.NewSigner(key, &jose.SignerOptions{}) From 7b8be4387a20a2ceeb5dfd7a229f308e1a6e01ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 13:37:53 +0200 Subject: [PATCH 415/502] chore(deps): bump github.com/go-jose/go-jose/v4 from 4.0.2 to 4.0.3 (#624) Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.0.2 to 4.0.3. - [Release notes](https://github.com/go-jose/go-jose/releases) - [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md) - [Commits](https://github.com/go-jose/go-jose/compare/v4.0.2...v4.0.3) --- updated-dependencies: - dependency-name: github.com/go-jose/go-jose/v4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 80906a4..c49ddb7 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 require ( github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/go-chi/chi/v5 v5.1.0 - github.com/go-jose/go-jose/v4 v4.0.2 + github.com/go-jose/go-jose/v4 v4.0.3 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 github.com/google/uuid v1.6.0 @@ -31,8 +31,8 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/otel/metric v1.28.0 // indirect go.opentelemetry.io/otel/trace v1.28.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.25.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.22.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 280feb7..affa77e 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ 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/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= -github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-jose/go-jose/v4 v4.0.3 h1:o8aphO8Hv6RPmH+GfzVuyf7YXSBibp+8YyHdOoDESGo= +github.com/go-jose/go-jose/v4 v4.0.3/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -62,8 +62,8 @@ go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+ go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -83,8 +83,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From b9bcd6aef9c19673fa9ef942a41ef987bf825a56 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 11:14:03 +0300 Subject: [PATCH 416/502] chore(deps): bump github.com/go-jose/go-jose/v4 from 4.0.3 to 4.0.4 (#625) Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.0.3 to 4.0.4. - [Release notes](https://github.com/go-jose/go-jose/releases) - [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md) - [Commits](https://github.com/go-jose/go-jose/compare/v4.0.3...v4.0.4) --- updated-dependencies: - dependency-name: github.com/go-jose/go-jose/v4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c49ddb7..a3b616b 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 require ( github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/go-chi/chi/v5 v5.1.0 - github.com/go-jose/go-jose/v4 v4.0.3 + github.com/go-jose/go-jose/v4 v4.0.4 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index affa77e..ac8e156 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ 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/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-jose/go-jose/v4 v4.0.3 h1:o8aphO8Hv6RPmH+GfzVuyf7YXSBibp+8YyHdOoDESGo= -github.com/go-jose/go-jose/v4 v4.0.3/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= +github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= +github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= From 8f80225a2039a9a4b504fbe1dbb0dde567ab2d13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:07:00 +0300 Subject: [PATCH 417/502] chore(deps): bump golang.org/x/oauth2 from 0.21.0 to 0.22.0 (#631) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.21.0 to 0.22.0. - [Commits](https://github.com/golang/oauth2/compare/v0.21.0...v0.22.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a3b616b..b5fa9c7 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/zitadel/logging v0.6.0 github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.28.0 - golang.org/x/oauth2 v0.21.0 + golang.org/x/oauth2 v0.22.0 golang.org/x/text v0.16.0 ) diff --git a/go.sum b/go.sum index ac8e156..a857be9 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= From 6f0a630ad407ec6c1ad389d46a22d1bbef9313a1 Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Tue, 6 Aug 2024 11:58:52 +0200 Subject: [PATCH 418/502] fix: overwrite redirect content length (#632) * fix: overwrite redirect content length * copy redirect struct headers --- pkg/op/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/op/server.go b/pkg/op/server.go index 829618c..6faee87 100644 --- a/pkg/op/server.go +++ b/pkg/op/server.go @@ -246,7 +246,7 @@ func NewRedirect(url string) *Redirect { } func (red *Redirect) writeOut(w http.ResponseWriter, r *http.Request) { - gu.MapMerge(r.Header, w.Header()) + gu.MapMerge(red.Header, w.Header()) http.Redirect(w, r, red.URL, http.StatusFound) } From b6f3b1e65b9142bd8b23342c0e2820841b2dac49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 9 Aug 2024 08:10:11 +0300 Subject: [PATCH 419/502] feat(op): allow returning of parent errors to client (#629) * feat(op): allow returning of parent errors to client * update godoc --------- Co-authored-by: Livio Spring --- pkg/oidc/error.go | 31 +++++++++++++++++++++++++++++++ pkg/oidc/error_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/pkg/oidc/error.go b/pkg/oidc/error.go index 2f0572d..1100f73 100644 --- a/pkg/oidc/error.go +++ b/pkg/oidc/error.go @@ -1,6 +1,7 @@ package oidc import ( + "encoding/json" "errors" "fmt" "log/slog" @@ -133,6 +134,24 @@ type Error struct { Description string `json:"error_description,omitempty" schema:"error_description,omitempty"` State string `json:"state,omitempty" schema:"state,omitempty"` redirectDisabled bool `schema:"-"` + returnParent bool `schema:"-"` +} + +func (e *Error) MarshalJSON() ([]byte, error) { + m := struct { + Error errorType `json:"error"` + ErrorDescription string `json:"error_description,omitempty"` + State string `json:"state,omitempty"` + Parent string `json:"parent,omitempty"` + }{ + Error: e.ErrorType, + ErrorDescription: e.Description, + State: e.State, + } + if e.returnParent { + m.Parent = e.Parent.Error() + } + return json.Marshal(m) } func (e *Error) Error() string { @@ -165,6 +184,18 @@ func (e *Error) WithParent(err error) *Error { return e } +// WithReturnParentToClient allows returning the set parent error to the HTTP client. +// Currently it only supports setting the parent inside JSON responses, not redirect URLs. +// As Go errors don't unmarshal well, only the marshaller is implemented for the moment. +// +// Warning: parent errors may contain sensitive data or unwanted details about the server status. +// Also, the `parent` field is not a standard error field and might confuse certain clients +// that require fully compliant responses. +func (e *Error) WithReturnParentToClient(b bool) *Error { + e.returnParent = b + return e +} + func (e *Error) WithDescription(desc string, args ...any) *Error { e.Description = fmt.Sprintf(desc, args...) return e diff --git a/pkg/oidc/error_test.go b/pkg/oidc/error_test.go index 2eeb4e6..40d30b1 100644 --- a/pkg/oidc/error_test.go +++ b/pkg/oidc/error_test.go @@ -1,11 +1,14 @@ package oidc import ( + "encoding/json" + "errors" "io" "log/slog" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestDefaultToServerError(t *testing.T) { @@ -151,3 +154,39 @@ func TestError_LogValue(t *testing.T) { }) } } + +func TestError_MarshalJSON(t *testing.T) { + tests := []struct { + name string + e *Error + want string + }{ + { + name: "simple error", + e: ErrAccessDenied(), + want: `{"error":"access_denied","error_description":"The authorization request was denied."}`, + }, + { + name: "with description", + e: ErrAccessDenied().WithDescription("oops"), + want: `{"error":"access_denied","error_description":"oops"}`, + }, + { + name: "with parent", + e: ErrServerError().WithParent(errors.New("oops")), + want: `{"error":"server_error"}`, + }, + { + name: "with return parent", + e: ErrServerError().WithParent(errors.New("oops")).WithReturnParentToClient(true), + want: `{"error":"server_error","parent":"oops"}`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := json.Marshal(tt.e) + require.NoError(t, err) + assert.JSONEq(t, tt.want, string(got)) + }) + } +} From de034c8d24498884e82adf7027059dec80aafd95 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Aug 2024 09:52:23 +0000 Subject: [PATCH 420/502] chore(deps): bump golang.org/x/text from 0.16.0 to 0.17.0 (#633) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.16.0 to 0.17.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.16.0...v0.17.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b5fa9c7..6533530 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.28.0 golang.org/x/oauth2 v0.22.0 - golang.org/x/text v0.16.0 + golang.org/x/text v0.17.0 ) require ( diff --git a/go.sum b/go.sum index a857be9..19c1724 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,8 @@ golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= From 0aa61b0b989fdfdecd02f923048ce80620a84d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 21 Aug 2024 10:29:14 +0300 Subject: [PATCH 421/502] fix(op): do not redirect to unverified uri on error (#640) Closes #627 --- pkg/op/auth_request.go | 48 +++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index fe73180..52fda2e 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -83,19 +83,27 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { if authReq.RequestParam != "" && authorizer.RequestObjectSupported() { err = ParseRequestObject(ctx, authReq, authorizer.Storage(), IssuerFromContext(ctx)) if err != nil { - AuthRequestError(w, r, authReq, err, authorizer) + AuthRequestError(w, r, nil, err, authorizer) return } } if authReq.ClientID == "" { - AuthRequestError(w, r, authReq, fmt.Errorf("auth request is missing client_id"), authorizer) + AuthRequestError(w, r, nil, fmt.Errorf("auth request is missing client_id"), authorizer) return } if authReq.RedirectURI == "" { - AuthRequestError(w, r, authReq, fmt.Errorf("auth request is missing redirect_uri"), authorizer) + AuthRequestError(w, r, nil, fmt.Errorf("auth request is missing redirect_uri"), authorizer) return } - validation := ValidateAuthRequest + + var client Client + validation := func(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier *IDTokenHintVerifier) (sub string, err error) { + client, err = authorizer.Storage().GetClientByClientID(ctx, authReq.ClientID) + if err != nil { + return "", oidc.ErrInvalidRequestRedirectURI().WithDescription("unable to retrieve client by id").WithParent(err) + } + return ValidateAuthRequestClient(ctx, authReq, client, verifier) + } if validater, ok := authorizer.(AuthorizeValidator); ok { validation = validater.ValidateAuthRequest } @@ -113,11 +121,6 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { AuthRequestError(w, r, authReq, oidc.DefaultToServerError(err, "unable to save auth request"), authorizer) return } - client, err := authorizer.Storage().GetClientByClientID(ctx, req.GetClientID()) - if err != nil { - AuthRequestError(w, r, req, oidc.DefaultToServerError(err, "unable to retrieve client by id"), authorizer) - return - } RedirectToLogin(req.GetID(), client, w, r) } @@ -212,26 +215,37 @@ func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oi authReq.RequestParam = "" } -// ValidateAuthRequest validates the authorize parameters and returns the userID of the id_token_hint if passed +// ValidateAuthRequest validates the authorize parameters and returns the userID of the id_token_hint if passed. +// +// Deprecated: Use [ValidateAuthRequestClient] to prevent querying for the Client twice. func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier *IDTokenHintVerifier) (sub string, err error) { ctx, span := tracer.Start(ctx, "ValidateAuthRequest") defer span.End() + client, err := storage.GetClientByClientID(ctx, authReq.ClientID) + if err != nil { + return "", oidc.ErrInvalidRequestRedirectURI().WithDescription("unable to retrieve client by id").WithParent(err) + } + return ValidateAuthRequestClient(ctx, authReq, client, verifier) +} + +// ValidateAuthRequestClient validates the Auth request against the passed client. +// If id_token_hint is part of the request, the subject of the token is returned. +func ValidateAuthRequestClient(ctx context.Context, authReq *oidc.AuthRequest, client Client, verifier *IDTokenHintVerifier) (sub string, err error) { + ctx, span := tracer.Start(ctx, "ValidateAuthRequestClient") + defer span.End() + + if err := ValidateAuthReqRedirectURI(client, authReq.RedirectURI, authReq.ResponseType); err != nil { + return "", err + } authReq.MaxAge, err = ValidateAuthReqPrompt(authReq.Prompt, authReq.MaxAge) if err != nil { return "", err } - client, err := storage.GetClientByClientID(ctx, authReq.ClientID) - if err != nil { - return "", oidc.DefaultToServerError(err, "unable to retrieve client by id") - } authReq.Scopes, err = ValidateAuthReqScopes(client, authReq.Scopes) if err != nil { return "", err } - if err := ValidateAuthReqRedirectURI(client, authReq.RedirectURI, authReq.ResponseType); err != nil { - return "", err - } if err := ValidateAuthReqResponseType(client, authReq.ResponseType); err != nil { return "", err } From 99301930edd3e37083107f8ca0685f446ef0d5eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 21 Aug 2024 10:32:13 +0300 Subject: [PATCH 422/502] feat(crypto): hash algorithm for EdDSA (#638) * feat(crypto): hash algorithm for EdDSA * update code comment * rp: modify keytype check to support EdDSA * example: signing algs from discovery --------- Co-authored-by: Livio Spring --- example/client/app/app.go | 1 + pkg/crypto/hash.go | 8 ++++++++ pkg/oidc/keyset.go | 17 +++++++++-------- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/example/client/app/app.go b/example/client/app/app.go index 448c530..0b9b19d 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -56,6 +56,7 @@ func main() { rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)), rp.WithHTTPClient(client), rp.WithLogger(logger), + rp.WithSigningAlgsFromDiscovery(), } if clientSecret == "" { options = append(options, rp.WithPKCE(cookieHandler)) diff --git a/pkg/crypto/hash.go b/pkg/crypto/hash.go index ab9f8c1..14acdee 100644 --- a/pkg/crypto/hash.go +++ b/pkg/crypto/hash.go @@ -21,6 +21,14 @@ func GetHashAlgorithm(sigAlgorithm jose.SignatureAlgorithm) (hash.Hash, error) { return sha512.New384(), nil case jose.RS512, jose.ES512, jose.PS512: return sha512.New(), nil + + // There is no published spec for this yet, but we have confirmation it will get published. + // There is consensus here: https://bitbucket.org/openid/connect/issues/1125/_hash-algorithm-for-eddsa-id-tokens + // Currently Go and go-jose only supports the ed25519 curve key for EdDSA, so we can safely assume sha512 here. + // It is unlikely ed448 will ever be supported: https://github.com/golang/go/issues/29390 + case jose.EdDSA: + return sha512.New(), nil + default: return nil, fmt.Errorf("%w: %q", ErrUnsupportedAlgorithm, sigAlgorithm) } diff --git a/pkg/oidc/keyset.go b/pkg/oidc/keyset.go index 833878d..a8b89b0 100644 --- a/pkg/oidc/keyset.go +++ b/pkg/oidc/keyset.go @@ -6,6 +6,7 @@ import ( "crypto/ed25519" "crypto/rsa" "errors" + "strings" jose "github.com/go-jose/go-jose/v4" ) @@ -92,17 +93,17 @@ func FindMatchingKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (k } func algToKeyType(key any, alg string) bool { - switch alg[0] { - case 'R', 'P': + if strings.HasPrefix(alg, "RS") || strings.HasPrefix(alg, "PS") { _, ok := key.(*rsa.PublicKey) return ok - case 'E': + } + if strings.HasPrefix(alg, "ES") { _, ok := key.(*ecdsa.PublicKey) return ok - case 'O': - _, ok := key.(*ed25519.PublicKey) - return ok - default: - return false } + if alg == string(jose.EdDSA) { + _, ok := key.(ed25519.PublicKey) + return ok + } + return false } From 1e75773eaadb655ea96cf37bc3f35ee2666f5a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 21 Aug 2024 10:34:26 +0300 Subject: [PATCH 423/502] fix(op): initialize http Headers in response objects (#637) * fix(op): initialize http Headers in response objects * fix test --------- Co-authored-by: Livio Spring --- pkg/op/error_test.go | 3 ++- pkg/op/server.go | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pkg/op/error_test.go b/pkg/op/error_test.go index 170039c..107f9d0 100644 --- a/pkg/op/error_test.go +++ b/pkg/op/error_test.go @@ -428,7 +428,8 @@ func TestTryErrorRedirect(t *testing.T) { parent: oidc.ErrInteractionRequired().WithDescription("sign in"), }, want: &Redirect{ - URL: "http://example.com/callback?error=interaction_required&error_description=sign+in&state=state1", + Header: make(http.Header), + URL: "http://example.com/callback?error=interaction_required&error_description=sign+in&state=state1", }, wantLog: `{ "level":"WARN", diff --git a/pkg/op/server.go b/pkg/op/server.go index 6faee87..b500e43 100644 --- a/pkg/op/server.go +++ b/pkg/op/server.go @@ -218,7 +218,8 @@ type Response struct { // without custom headers. func NewResponse(data any) *Response { return &Response{ - Data: data, + Header: make(http.Header), + Data: data, } } @@ -242,7 +243,10 @@ type Redirect struct { } func NewRedirect(url string) *Redirect { - return &Redirect{URL: url} + return &Redirect{ + Header: make(http.Header), + URL: url, + } } func (red *Redirect) writeOut(w http.ResponseWriter, r *http.Request) { From 67688db4c114c5b5595bf4ec784f15a74415413a Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Mon, 26 Aug 2024 01:11:01 -0700 Subject: [PATCH 424/502] fix: client assertions for Okta (#636) * fix client assertions for Okta * review feedback --- pkg/client/rp/relying_party.go | 2 +- pkg/oidc/token_request.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 029a897..e6fa078 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -541,7 +541,7 @@ func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp R rp.CookieHandler().DeleteCookie(w, pkceCode) } if rp.Signer() != nil { - assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer()}, time.Hour, rp.Signer()) + assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer(), rp.OAuthConfig().Endpoint.TokenURL}, time.Hour, rp.Signer()) if err != nil { unauthorizedError(w, r, "failed to build assertion: "+err.Error(), state, rp) return diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index f3b2ec4..dadb205 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -72,10 +72,10 @@ type AccessTokenRequest struct { Code string `schema:"code"` RedirectURI string `schema:"redirect_uri"` ClientID string `schema:"client_id"` - ClientSecret string `schema:"client_secret"` - CodeVerifier string `schema:"code_verifier"` - ClientAssertion string `schema:"client_assertion"` - ClientAssertionType string `schema:"client_assertion_type"` + ClientSecret string `schema:"client_secret,omitempty"` + CodeVerifier string `schema:"code_verifier,omitempty"` + ClientAssertion string `schema:"client_assertion,omitempty"` + ClientAssertionType string `schema:"client_assertion_type,omitempty"` } func (a *AccessTokenRequest) GrantType() GrantType { From 52e8b651d36e638d462a00162af7fab16cb74734 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 08:13:38 +0000 Subject: [PATCH 425/502] chore(deps): bump go.opentelemetry.io/otel from 1.28.0 to 1.29.0 (#643) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) from 1.28.0 to 1.29.0. - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.28.0...v1.29.0) --- updated-dependencies: - dependency-name: go.opentelemetry.io/otel dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tim Möhlmann --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 6533530..a4b91e7 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/stretchr/testify v1.9.0 github.com/zitadel/logging v0.6.0 github.com/zitadel/schema v1.3.0 - go.opentelemetry.io/otel v1.28.0 + go.opentelemetry.io/otel v1.29.0 golang.org/x/oauth2 v0.22.0 golang.org/x/text v0.17.0 ) @@ -29,8 +29,8 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect - go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect golang.org/x/crypto v0.25.0 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/sys v0.22.0 // indirect diff --git a/go.sum b/go.sum index 19c1724..5948e97 100644 --- a/go.sum +++ b/go.sum @@ -54,12 +54,12 @@ github.com/zitadel/logging v0.6.0 h1:t5Nnt//r+m2ZhhoTmoPX+c96pbMarqJvW1Vq6xFTank github.com/zitadel/logging v0.6.0/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= From 5e464b4ed8d0fd765aa218d11f4f6bd53e1b7275 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Aug 2024 09:58:10 +0300 Subject: [PATCH 426/502] chore(deps): bump github.com/rs/cors from 1.11.0 to 1.11.1 (#645) Bumps [github.com/rs/cors](https://github.com/rs/cors) from 1.11.0 to 1.11.1. - [Commits](https://github.com/rs/cors/compare/v1.11.0...v1.11.1) --- updated-dependencies: - dependency-name: github.com/rs/cors dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a4b91e7..2fcdc8a 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/jeremija/gosubmit v0.2.7 github.com/muhlemmer/gu v0.3.1 github.com/muhlemmer/httpforwarded v0.1.0 - github.com/rs/cors v1.11.0 + github.com/rs/cors v1.11.1 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.0 github.com/zitadel/logging v0.6.0 diff --git a/go.sum b/go.sum index 5948e97..e78b896 100644 --- a/go.sum +++ b/go.sum @@ -41,8 +41,8 @@ github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/ github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= -github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From e1633bdb931f39688b7a43be8318e5a86c71f3e7 Mon Sep 17 00:00:00 2001 From: lanseg Date: Tue, 3 Sep 2024 10:13:06 +0200 Subject: [PATCH 427/502] feat: Define redirect uris with env variables (#644) Co-authored-by: Andrey Rusakov --- example/server/exampleop/op.go | 9 --------- example/server/main.go | 7 +++++++ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/example/server/exampleop/op.go b/example/server/exampleop/op.go index e8ef892..8f55b0a 100644 --- a/example/server/exampleop/op.go +++ b/example/server/exampleop/op.go @@ -12,7 +12,6 @@ import ( "github.com/zitadel/logging" "golang.org/x/text/language" - "github.com/zitadel/oidc/v3/example/server/storage" "github.com/zitadel/oidc/v3/pkg/op" ) @@ -20,14 +19,6 @@ const ( pathLoggedOut = "/logged-out" ) -func init() { - storage.RegisterClients( - storage.NativeClient("native"), - storage.WebClient("web", "secret"), - storage.WebClient("api", "secret"), - ) -} - type Storage interface { op.Storage authenticate diff --git a/example/server/main.go b/example/server/main.go index a2ad190..da8e73f 100644 --- a/example/server/main.go +++ b/example/server/main.go @@ -5,6 +5,7 @@ import ( "log/slog" "net/http" "os" + "strings" "github.com/zitadel/oidc/v3/example/server/exampleop" "github.com/zitadel/oidc/v3/example/server/storage" @@ -16,6 +17,12 @@ func main() { //which gives us the issuer: http://localhost:9998/ issuer := fmt.Sprintf("http://localhost:%s/", port) + storage.RegisterClients( + storage.NativeClient("native", strings.Split(os.Getenv("REDIRECT_URI"), ",")...), + storage.WebClient("web", "secret"), + storage.WebClient("api", "secret"), + ) + // the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations // this might be the layer for accessing your database // in this example it will be handled in-memory From 6c28e8cb4b912d5d56d70da77dc0dd8514437d44 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 13:31:08 +0300 Subject: [PATCH 428/502] chore(deps): bump golang.org/x/oauth2 from 0.22.0 to 0.23.0 (#647) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.22.0 to 0.23.0. - [Commits](https://github.com/golang/oauth2/compare/v0.22.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2fcdc8a..9a64a1c 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/zitadel/logging v0.6.0 github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.29.0 - golang.org/x/oauth2 v0.22.0 + golang.org/x/oauth2 v0.23.0 golang.org/x/text v0.17.0 ) diff --git a/go.sum b/go.sum index e78b896..d98c579 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= -golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= From 98c1ab755dafd7117ebbe76c4c42604b704e93ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 13:49:22 +0300 Subject: [PATCH 429/502] chore(deps): bump golang.org/x/text from 0.17.0 to 0.18.0 (#648) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.17.0 to 0.18.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.17.0...v0.18.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9a64a1c..c0b2b1a 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.29.0 golang.org/x/oauth2 v0.23.0 - golang.org/x/text v0.17.0 + golang.org/x/text v0.18.0 ) require ( diff --git a/go.sum b/go.sum index d98c579..87d510a 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,8 @@ golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= From b555396744ffca5af3d8ede409ec262d07b48be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 10 Sep 2024 12:50:54 +0300 Subject: [PATCH 430/502] fix(oidc): set client ID to access token JWT (#650) * fix(oidc): set client ID to access token JWT * fix test --- pkg/oidc/token.go | 1 + pkg/oidc/token_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index 5b18dac..a829df4 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -117,6 +117,7 @@ func NewAccessTokenClaims(issuer, subject string, audience []string, expiration Expiration: FromTime(expiration), IssuedAt: FromTime(now), NotBefore: FromTime(now), + ClientID: clientID, JWTID: jwtid, }, } diff --git a/pkg/oidc/token_test.go b/pkg/oidc/token_test.go index ccc3467..7847cb5 100644 --- a/pkg/oidc/token_test.go +++ b/pkg/oidc/token_test.go @@ -145,6 +145,7 @@ func TestNewAccessTokenClaims(t *testing.T) { Subject: "hello@me.com", Audience: Audience{"foo"}, Expiration: 12345, + ClientID: "foo", JWTID: "900", }, } From 3b64e792ed1c01daf6bb3320a8da4ffa346753c2 Mon Sep 17 00:00:00 2001 From: Ayato Date: Fri, 20 Sep 2024 18:33:28 +0900 Subject: [PATCH 431/502] feat(oidc): return defined error when discovery failed (#653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(oidc): return defined error when discovery failed * Use errors.Join() to join errors Co-authored-by: Tim Möhlmann * Remove unnecessary field Co-authored-by: Tim Möhlmann * Fix order and message Co-authored-by: Tim Möhlmann * Fix error order * Simplify error assertion Co-authored-by: Tim Möhlmann --------- Co-authored-by: Tim Möhlmann --- pkg/client/client.go | 2 +- pkg/client/client_test.go | 18 +++++++++++------- pkg/oidc/verifier.go | 1 + 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index 990da9b..56417b5 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -42,7 +42,7 @@ func Discover(ctx context.Context, issuer string, httpClient *http.Client, wellK discoveryConfig := new(oidc.DiscoveryConfiguration) err = httphelper.HttpRequest(httpClient, req, &discoveryConfig) if err != nil { - return nil, err + return nil, errors.Join(oidc.ErrDiscoveryFailed, err) } if logger, ok := logging.FromContext(ctx); ok { logger.Debug("discover", "config", discoveryConfig) diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index e06c825..1046941 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/zitadel/oidc/v3/pkg/oidc" ) func TestDiscover(t *testing.T) { @@ -22,7 +23,7 @@ func TestDiscover(t *testing.T) { name string args args wantFields *wantFields - wantErr bool + wantErr error }{ { name: "spotify", // https://github.com/zitadel/oidc/issues/406 @@ -32,17 +33,20 @@ func TestDiscover(t *testing.T) { wantFields: &wantFields{ UILocalesSupported: true, }, - wantErr: false, + wantErr: nil, + }, + { + name: "discovery failed", + args: args{ + issuer: "https://example.com", + }, + wantErr: oidc.ErrDiscoveryFailed, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := Discover(context.Background(), tt.args.issuer, http.DefaultClient, tt.args.wellKnownUrl...) - if tt.wantErr { - assert.Error(t, err) - return - } - require.NoError(t, err) + require.ErrorIs(t, err, tt.wantErr) if tt.wantFields == nil { return } diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go index cb66676..f580da6 100644 --- a/pkg/oidc/verifier.go +++ b/pkg/oidc/verifier.go @@ -41,6 +41,7 @@ type IDClaims interface { var ( ErrParse = errors.New("parsing of request failed") ErrIssuerInvalid = errors.New("issuer does not match") + ErrDiscoveryFailed = errors.New("OpenID Provider Configuration Discovery has failed") ErrSubjectMissing = errors.New("subject missing") ErrAudience = errors.New("audience is not valid") ErrAzpMissing = errors.New("authorized party is not set. If Token is valid for multiple audiences, azp must not be empty") From 61c3bb887b5827d9307b1302cb1d6cd5ab052337 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:46:21 +0200 Subject: [PATCH 432/502] chore(deps): bump github.com/zitadel/logging from 0.6.0 to 0.6.1 (#657) Bumps [github.com/zitadel/logging](https://github.com/zitadel/logging) from 0.6.0 to 0.6.1. - [Release notes](https://github.com/zitadel/logging/releases) - [Changelog](https://github.com/zitadel/logging/blob/main/.releaserc.js) - [Commits](https://github.com/zitadel/logging/compare/v0.6.0...v0.6.1) --- updated-dependencies: - dependency-name: github.com/zitadel/logging dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c0b2b1a..041070b 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/rs/cors v1.11.1 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.0 - github.com/zitadel/logging v0.6.0 + github.com/zitadel/logging v0.6.1 github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.29.0 golang.org/x/oauth2 v0.23.0 diff --git a/go.sum b/go.sum index 87d510a..2ca1cfa 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/zitadel/logging v0.6.0 h1:t5Nnt//r+m2ZhhoTmoPX+c96pbMarqJvW1Vq6xFTank= -github.com/zitadel/logging v0.6.0/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= +github.com/zitadel/logging v0.6.1 h1:Vyzk1rl9Kq9RCevcpX6ujUaTYFX43aa4LkvV1TvUk+Y= +github.com/zitadel/logging v0.6.1/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= From 97d7b28fc080f9929e590ef1989880b0762e0098 Mon Sep 17 00:00:00 2001 From: cui fliter Date: Fri, 4 Oct 2024 19:56:57 +0800 Subject: [PATCH 433/502] fix: fix slice init length (#658) --- example/server/storage/oidc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/server/storage/oidc.go b/example/server/storage/oidc.go index 9cd08d9..22c0295 100644 --- a/example/server/storage/oidc.go +++ b/example/server/storage/oidc.go @@ -121,7 +121,7 @@ func (a *AuthRequest) Done() bool { } func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string { - prompts := make([]string, len(oidcPrompt)) + prompts := make([]string, 0, len(oidcPrompt)) for _, oidcPrompt := range oidcPrompt { switch oidcPrompt { case oidc.PromptNone, From 2abae36bd9b7c37c3d4c48cf07aa508ab78bfcef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 18:39:28 +0300 Subject: [PATCH 434/502] chore(deps): bump golang.org/x/text from 0.18.0 to 0.19.0 (#661) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.18.0 to 0.19.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.18.0...v0.19.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 041070b..caf84de 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.29.0 golang.org/x/oauth2 v0.23.0 - golang.org/x/text v0.18.0 + golang.org/x/text v0.19.0 ) require ( diff --git a/go.sum b/go.sum index 2ca1cfa..8d4c0db 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,8 @@ golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= From 5ae555e19136066760d02e10af451464c6a3e3c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:00:43 +0300 Subject: [PATCH 435/502] chore(deps): bump codecov/codecov-action from 4.5.0 to 4.6.0 (#662) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.5.0 to 4.6.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4.5.0...v4.6.0) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 48690cf..66d68b6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v4.5.0 + - uses: codecov/codecov-action@v4.6.0 with: file: ./profile.cov name: codecov-go From 9f7cbb0dbfc39ce85ecb2f2024d6f20d44e8fcef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 14:12:28 +0300 Subject: [PATCH 436/502] chore(deps): bump github.com/bmatcuk/doublestar/v4 from 4.6.1 to 4.7.1 (#666) Bumps [github.com/bmatcuk/doublestar/v4](https://github.com/bmatcuk/doublestar) from 4.6.1 to 4.7.1. - [Release notes](https://github.com/bmatcuk/doublestar/releases) - [Commits](https://github.com/bmatcuk/doublestar/compare/v4.6.1...v4.7.1) --- updated-dependencies: - dependency-name: github.com/bmatcuk/doublestar/v4 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index caf84de..f50972a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/zitadel/oidc/v3 go 1.21 require ( - github.com/bmatcuk/doublestar/v4 v4.6.1 + github.com/bmatcuk/doublestar/v4 v4.7.1 github.com/go-chi/chi/v5 v5.1.0 github.com/go-jose/go-jose/v4 v4.0.4 github.com/golang/mock v1.6.0 diff --git a/go.sum b/go.sum index 8d4c0db..91de9bf 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= -github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q= +github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 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= From 24869d281155064b0a4a6339ad641ba76bac6177 Mon Sep 17 00:00:00 2001 From: lanseg Date: Mon, 21 Oct 2024 20:59:28 +0200 Subject: [PATCH 437/502] feat(example): Allow configuring some parameters with env variables (#663) Co-authored-by: Andrey Rusakov --- README.md | 38 ++++++++++++-- example/server/config/config.go | 40 +++++++++++++++ example/server/config/config_test.go | 77 ++++++++++++++++++++++++++++ example/server/main.go | 44 ++++++++++------ example/server/storage/user.go | 14 +++++ example/server/storage/user_test.go | 70 +++++++++++++++++++++++++ 6 files changed, 262 insertions(+), 21 deletions(-) create mode 100644 example/server/config/config.go create mode 100644 example/server/config/config_test.go create mode 100644 example/server/storage/user_test.go diff --git a/README.md b/README.md index 01d7d47..c1ff0aa 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Whenever possible we tried to reuse / extend existing packages like `OAuth2 for The most important packages of the library:
 /pkg
-    /client            clients using the OP for retrieving, exchanging and verifying tokens       
+    /client            clients using the OP for retrieving, exchanging and verifying tokens
         /rp            definition and implementation of an OIDC Relying Party (client)
         /rs            definition and implementation of an OAuth Resource Server (API)
     /op                definition and implementation of an OIDC OpenID Provider (server)
@@ -55,14 +55,14 @@ CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998/ SCOPES="openid
 ```
 
 - open http://localhost:9999/login in your browser
-- you will be redirected to op server and the login UI 
+- you will be redirected to op server and the login UI
 - login with user `test-user@localhost` and password `verysecure`
 - the OP will redirect you to the client app, which displays the user info
 
 for the dynamic issuer, just start it with:
 ```bash
 go run github.com/zitadel/oidc/v3/example/server/dynamic
-``` 
+```
 the oidc web client above will still work, but if you add `oidc.local` (pointing to 127.0.0.1) in your hosts file you can also start it with:
 ```bash
 CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v3/example/client/app
@@ -70,6 +70,36 @@ CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid
 
 > Note: Usernames are suffixed with the hostname (`test-user@localhost` or `test-user@oidc.local`)
 
+### Server configuration
+
+Example server allows extra configuration using environment variables and could be used for end to
+end testing of your services.
+
+| Name          | Format                               | Description                           |
+|---------------|--------------------------------------|---------------------------------------|
+| PORT          | Number between 1 and 65535           | OIDC listen port                      |
+| REDIRECT_URI  | Comma-separated URIs                 | List of allowed redirect URIs         |
+| USERS_FILE    | Path to json in local filesystem     | Users with their data and credentials |
+
+Here is json equivalent for one of the default users
+```json
+{
+    "id2": {
+        "ID":                "id2",
+        "Username":          "test-user2",
+        "Password":          "verysecure",
+        "FirstName":         "Test",
+        "LastName":          "User2",
+        "Email":             "test-user2@zitadel.ch",
+        "EmailVerified":     true,
+        "Phone":             "",
+        "PhoneVerified":     false,
+        "PreferredLanguage": "DE",
+        "IsAdmin":           false
+    }
+}
+```
+
 ## Features
 
 |                      | Relying party | OpenID Provider | Specification                             |
@@ -115,7 +145,7 @@ For your convenience you can find the relevant guides linked below.
 
 ## Supported Go Versions
 
-For security reasons, we only support and recommend the use of one of the latest two Go versions (:white_check_mark:).  
+For security reasons, we only support and recommend the use of one of the latest two Go versions (:white_check_mark:).
 Versions that also build are marked with :warning:.
 
 | Version | Supported          |
diff --git a/example/server/config/config.go b/example/server/config/config.go
new file mode 100644
index 0000000..96837d4
--- /dev/null
+++ b/example/server/config/config.go
@@ -0,0 +1,40 @@
+package config
+
+import (
+	"os"
+	"strings"
+)
+
+const (
+	// default port for the http server to run
+	DefaultIssuerPort = "9998"
+)
+
+type Config struct {
+	Port        string
+	RedirectURI []string
+	UsersFile   string
+}
+
+// FromEnvVars loads configuration parameters from environment variables.
+// If there is no such variable defined, then use default values.
+func FromEnvVars(defaults *Config) *Config {
+	if defaults == nil {
+		defaults = &Config{}
+	}
+	cfg := &Config{
+		Port:        defaults.Port,
+		RedirectURI: defaults.RedirectURI,
+		UsersFile:   defaults.UsersFile,
+	}
+	if value, ok := os.LookupEnv("PORT"); ok {
+		cfg.Port = value
+	}
+	if value, ok := os.LookupEnv("USERS_FILE"); ok {
+		cfg.UsersFile = value
+	}
+	if value, ok := os.LookupEnv("REDIRECT_URI"); ok {
+		cfg.RedirectURI = strings.Split(value, ",")
+	}
+	return cfg
+}
diff --git a/example/server/config/config_test.go b/example/server/config/config_test.go
new file mode 100644
index 0000000..3b73c0b
--- /dev/null
+++ b/example/server/config/config_test.go
@@ -0,0 +1,77 @@
+package config
+
+import (
+	"fmt"
+	"os"
+	"testing"
+)
+
+func TestFromEnvVars(t *testing.T) {
+
+	for _, tc := range []struct {
+		name     string
+		env      map[string]string
+		defaults *Config
+		want     *Config
+	}{
+		{
+			name: "no vars, no default values",
+			env:  map[string]string{},
+			want: &Config{},
+		},
+		{
+			name: "no vars, only defaults",
+			env:  map[string]string{},
+			defaults: &Config{
+				Port:        "6666",
+				UsersFile:   "/default/user/path",
+				RedirectURI: []string{"re", "direct", "uris"},
+			},
+			want: &Config{
+				Port:        "6666",
+				UsersFile:   "/default/user/path",
+				RedirectURI: []string{"re", "direct", "uris"},
+			},
+		},
+		{
+			name: "overriding default values",
+			env: map[string]string{
+				"PORT":         "1234",
+				"USERS_FILE":   "/path/to/users",
+				"REDIRECT_URI": "http://redirect/redirect",
+			},
+			defaults: &Config{
+				Port:        "6666",
+				UsersFile:   "/default/user/path",
+				RedirectURI: []string{"re", "direct", "uris"},
+			},
+			want: &Config{
+				Port:        "1234",
+				UsersFile:   "/path/to/users",
+				RedirectURI: []string{"http://redirect/redirect"},
+			},
+		},
+		{
+			name: "multiple redirect uris",
+			env: map[string]string{
+				"REDIRECT_URI": "http://host_1,http://host_2,http://host_3",
+			},
+			want: &Config{
+				RedirectURI: []string{
+					"http://host_1", "http://host_2", "http://host_3",
+				},
+			},
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			os.Clearenv()
+			for k, v := range tc.env {
+				os.Setenv(k, v)
+			}
+			cfg := FromEnvVars(tc.defaults)
+			if fmt.Sprint(cfg) != fmt.Sprint(tc.want) {
+				t.Errorf("Expected FromEnvVars()=%q, but got %q", tc.want, cfg)
+			}
+		})
+	}
+}
diff --git a/example/server/main.go b/example/server/main.go
index da8e73f..36816d6 100644
--- a/example/server/main.go
+++ b/example/server/main.go
@@ -5,20 +5,33 @@ import (
 	"log/slog"
 	"net/http"
 	"os"
-	"strings"
 
+	"github.com/zitadel/oidc/v3/example/server/config"
 	"github.com/zitadel/oidc/v3/example/server/exampleop"
 	"github.com/zitadel/oidc/v3/example/server/storage"
 )
 
+func getUserStore(cfg *config.Config) (storage.UserStore, error) {
+	if cfg.UsersFile == "" {
+		return storage.NewUserStore(fmt.Sprintf("http://localhost:%s/", cfg.Port)), nil
+	}
+	return storage.StoreFromFile(cfg.UsersFile)
+}
+
 func main() {
-	//we will run on :9998
-	port := "9998"
+	cfg := config.FromEnvVars(&config.Config{Port: "9998"})
+	logger := slog.New(
+		slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
+			AddSource: true,
+			Level:     slog.LevelDebug,
+		}),
+	)
+
 	//which gives us the issuer: http://localhost:9998/
-	issuer := fmt.Sprintf("http://localhost:%s/", port)
+	issuer := fmt.Sprintf("http://localhost:%s/", cfg.Port)
 
 	storage.RegisterClients(
-		storage.NativeClient("native", strings.Split(os.Getenv("REDIRECT_URI"), ",")...),
+		storage.NativeClient("native", cfg.RedirectURI...),
 		storage.WebClient("web", "secret"),
 		storage.WebClient("api", "secret"),
 	)
@@ -26,23 +39,20 @@ func main() {
 	// the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations
 	// this might be the layer for accessing your database
 	// in this example it will be handled in-memory
-	storage := storage.NewStorage(storage.NewUserStore(issuer))
-
-	logger := slog.New(
-		slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
-			AddSource: true,
-			Level:     slog.LevelDebug,
-		}),
-	)
+	store, err := getUserStore(cfg)
+	if err != nil {
+		logger.Error("cannot create UserStore", "error", err)
+		os.Exit(1)
+	}
+	storage := storage.NewStorage(store)
 	router := exampleop.SetupServer(issuer, storage, logger, false)
 
 	server := &http.Server{
-		Addr:    ":" + port,
+		Addr:    ":" + cfg.Port,
 		Handler: router,
 	}
-	logger.Info("server listening, press ctrl+c to stop", "addr", fmt.Sprintf("http://localhost:%s/", port))
-	err := server.ListenAndServe()
-	if err != http.ErrServerClosed {
+	logger.Info("server listening, press ctrl+c to stop", "addr", issuer)
+	if server.ListenAndServe() != http.ErrServerClosed {
 		logger.Error("server terminated", "error", err)
 		os.Exit(1)
 	}
diff --git a/example/server/storage/user.go b/example/server/storage/user.go
index 173daef..ed8cdfa 100644
--- a/example/server/storage/user.go
+++ b/example/server/storage/user.go
@@ -2,6 +2,8 @@ package storage
 
 import (
 	"crypto/rsa"
+	"encoding/json"
+	"os"
 	"strings"
 
 	"golang.org/x/text/language"
@@ -35,6 +37,18 @@ type userStore struct {
 	users map[string]*User
 }
 
+func StoreFromFile(path string) (UserStore, error) {
+	users := map[string]*User{}
+	data, err := os.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+	if err := json.Unmarshal(data, &users); err != nil {
+		return nil, err
+	}
+	return userStore{users}, nil
+}
+
 func NewUserStore(issuer string) UserStore {
 	hostname := strings.Split(strings.Split(issuer, "://")[1], ":")[0]
 	return userStore{
diff --git a/example/server/storage/user_test.go b/example/server/storage/user_test.go
new file mode 100644
index 0000000..c2e2212
--- /dev/null
+++ b/example/server/storage/user_test.go
@@ -0,0 +1,70 @@
+package storage
+
+import (
+	"os"
+	"path"
+	"reflect"
+	"testing"
+
+	"golang.org/x/text/language"
+)
+
+func TestStoreFromFile(t *testing.T) {
+	for _, tc := range []struct {
+		name       string
+		pathToFile string
+		content    string
+		want       UserStore
+		wantErr    bool
+	}{
+		{
+			name:       "normal user file",
+			pathToFile: "userfile.json",
+			content: `{
+				"id1": {
+					"ID":                "id1",
+					"EmailVerified":     true,
+					"PreferredLanguage": "DE"
+				}
+			}`,
+			want: userStore{map[string]*User{
+				"id1": {
+					ID:                "id1",
+					EmailVerified:     true,
+					PreferredLanguage: language.German,
+				},
+			}},
+		},
+		{
+			name:       "malformed file",
+			pathToFile: "whatever",
+			content:    "not a json just a text",
+			wantErr:    true,
+		},
+		{
+			name:       "not existing file",
+			pathToFile: "what/ever/file",
+			wantErr:    true,
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			actualPath := path.Join(t.TempDir(), tc.pathToFile)
+
+			if tc.content != "" && tc.pathToFile != "" {
+				if err := os.WriteFile(actualPath, []byte(tc.content), 0666); err != nil {
+					t.Fatalf("cannot create file with test content: %q", tc.content)
+				}
+			}
+			result, err := StoreFromFile(actualPath)
+			if err != nil && !tc.wantErr {
+				t.Errorf("StoreFromFile(%q) returned unexpected error %q", tc.pathToFile, err)
+			} else if err == nil && tc.wantErr {
+				t.Errorf("StoreFromFile(%q) did not return an expected error", tc.pathToFile)
+			}
+			if !tc.wantErr && !reflect.DeepEqual(tc.want, result.(userStore)) {
+				t.Errorf("expected StoreFromFile(%q) = %v, but got %v",
+					tc.pathToFile, tc.want, result)
+			}
+		})
+	}
+}

From f1e4cb22456afeb42d6d16d29a37231ad99224db Mon Sep 17 00:00:00 2001
From: Livio Spring 
Date: Wed, 30 Oct 2024 09:44:31 +0100
Subject: [PATCH 438/502] feat(OP): add back channel logout support (#671)

* feat: add configuration support for back channel logout

* logout token

* indicate back channel logout support in discovery endpoint
---
 README.md                         | 28 ++++++++++++-----------
 pkg/oidc/discovery.go             |  8 +++++++
 pkg/oidc/token.go                 | 37 +++++++++++++++++++++++++++++++
 pkg/oidc/token_test.go            | 36 ++++++++++++++++++++++++++++++
 pkg/op/config.go                  |  3 +++
 pkg/op/discovery.go               |  4 ++++
 pkg/op/mock/configuration.mock.go | 28 +++++++++++++++++++++++
 pkg/op/op.go                      | 30 ++++++++++++++++---------
 8 files changed, 151 insertions(+), 23 deletions(-)

diff --git a/README.md b/README.md
index c1ff0aa..b102815 100644
--- a/README.md
+++ b/README.md
@@ -102,19 +102,20 @@ Here is json equivalent for one of the default users
 
 ## Features
 
-|                      | Relying party | OpenID Provider | Specification                             |
-| -------------------- | ------------- | --------------- | ----------------------------------------- |
-| Code Flow            | yes           | yes             | OpenID Connect Core 1.0, [Section 3.1][1] |
-| Implicit Flow        | no[^1]        | yes             | OpenID Connect Core 1.0, [Section 3.2][2] |
-| Hybrid Flow          | no            | not yet         | OpenID Connect Core 1.0, [Section 3.3][3] |
-| Client Credentials   | yes           | yes             | OpenID Connect Core 1.0, [Section 9][4]   |
-| Refresh Token        | yes           | yes             | OpenID Connect Core 1.0, [Section 12][5]  |
-| Discovery            | yes           | yes             | OpenID Connect [Discovery][6] 1.0         |
-| JWT Profile          | yes           | yes             | [RFC 7523][7]                             |
-| PKCE                 | yes           | yes             | [RFC 7636][8]                             |
-| Token Exchange       | yes           | yes             | [RFC 8693][9]                             |
-| Device Authorization | yes           | yes             | [RFC 8628][10]                            |
-| mTLS                 | not yet       | not yet         | [RFC 8705][11]                            |
+|                      | Relying party | OpenID Provider | Specification                                |
+|----------------------| ------------- | --------------- |----------------------------------------------|
+| Code Flow            | yes           | yes             | OpenID Connect Core 1.0, [Section 3.1][1]    |
+| Implicit Flow        | no[^1]        | yes             | OpenID Connect Core 1.0, [Section 3.2][2]    |
+| Hybrid Flow          | no            | not yet         | OpenID Connect Core 1.0, [Section 3.3][3]    |
+| Client Credentials   | yes           | yes             | OpenID Connect Core 1.0, [Section 9][4]      |
+| Refresh Token        | yes           | yes             | OpenID Connect Core 1.0, [Section 12][5]     |
+| Discovery            | yes           | yes             | OpenID Connect [Discovery][6] 1.0            |
+| JWT Profile          | yes           | yes             | [RFC 7523][7]                                |
+| PKCE                 | yes           | yes             | [RFC 7636][8]                                |
+| Token Exchange       | yes           | yes             | [RFC 8693][9]                                |
+| Device Authorization | yes           | yes             | [RFC 8628][10]                               |
+| mTLS                 | not yet       | not yet         | [RFC 8705][11]                               |
+| Back-Channel Logout  | not yet       | yes             | OpenID Connect [Back-Channel Logout][12] 1.0 |
 
 [1]:  "3.1. Authentication using the Authorization Code Flow"
 [2]:  "3.2. Authentication using the Implicit Flow"
@@ -127,6 +128,7 @@ Here is json equivalent for one of the default users
 [9]:  "OAuth 2.0 Token Exchange"
 [10]:  "OAuth 2.0 Device Authorization Grant"
 [11]:  "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens"
+[12]:  "OpenID Connect Back-Channel Logout 1.0 incorporating errata set 1"
 
 ## Contributors
 
diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go
index 14fce5e..62288d1 100644
--- a/pkg/oidc/discovery.go
+++ b/pkg/oidc/discovery.go
@@ -145,6 +145,14 @@ type DiscoveryConfiguration struct {
 
 	// OPTermsOfServiceURI is a URL the OpenID Provider provides to the person registering the Client to read about OpenID Provider's terms of service.
 	OPTermsOfServiceURI string `json:"op_tos_uri,omitempty"`
+
+	// BackChannelLogoutSupported specifies whether the OP supports back-channel logout (https://openid.net/specs/openid-connect-backchannel-1_0.html),
+	// with true indicating support. If omitted, the default value is false.
+	BackChannelLogoutSupported bool `json:"backchannel_logout_supported,omitempty"`
+
+	// BackChannelLogoutSessionSupported specifies whether the OP can pass a sid (session ID) Claim in the Logout Token to identify the RP session with the OP.
+	// If supported, the sid Claim is also included in ID Tokens issued by the OP. If omitted, the default value is false.
+	BackChannelLogoutSessionSupported bool `json:"backchannel_logout_session_supported,omitempty"`
 }
 
 type AuthMethod string
diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go
index a829df4..e57d91e 100644
--- a/pkg/oidc/token.go
+++ b/pkg/oidc/token.go
@@ -382,3 +382,40 @@ type TokenExchangeResponse struct {
 	// if the requested_token_type was Access Token and scope contained openid.
 	IDToken string `json:"id_token,omitempty"`
 }
+
+type LogoutTokenClaims struct {
+	Issuer     string         `json:"iss,omitempty"`
+	Subject    string         `json:"sub,omitempty"`
+	Audience   Audience       `json:"aud,omitempty"`
+	IssuedAt   Time           `json:"iat,omitempty"`
+	Expiration Time           `json:"exp,omitempty"`
+	JWTID      string         `json:"jti,omitempty"`
+	Events     map[string]any `json:"events,omitempty"`
+	SessionID  string         `json:"sid,omitempty"`
+	Claims     map[string]any `json:"-"`
+}
+
+type ltcAlias LogoutTokenClaims
+
+func (i *LogoutTokenClaims) MarshalJSON() ([]byte, error) {
+	return mergeAndMarshalClaims((*ltcAlias)(i), i.Claims)
+}
+
+func (i *LogoutTokenClaims) UnmarshalJSON(data []byte) error {
+	return unmarshalJSONMulti(data, (*ltcAlias)(i), &i.Claims)
+}
+
+func NewLogoutTokenClaims(issuer, subject string, audience Audience, expiration time.Time, jwtID, sessionID string, skew time.Duration) *LogoutTokenClaims {
+	return &LogoutTokenClaims{
+		Issuer:     issuer,
+		Subject:    subject,
+		Audience:   audience,
+		IssuedAt:   FromTime(time.Now().Add(-skew)),
+		Expiration: FromTime(expiration),
+		JWTID:      jwtID,
+		Events: map[string]any{
+			"http://schemas.openid.net/event/backchannel-logout": struct{}{},
+		},
+		SessionID: sessionID,
+	}
+}
diff --git a/pkg/oidc/token_test.go b/pkg/oidc/token_test.go
index 7847cb5..621cdbc 100644
--- a/pkg/oidc/token_test.go
+++ b/pkg/oidc/token_test.go
@@ -242,3 +242,39 @@ func TestIDTokenClaims_GetUserInfo(t *testing.T) {
 	got := idTokenData.GetUserInfo()
 	assert.Equal(t, want, got)
 }
+
+func TestNewLogoutTokenClaims(t *testing.T) {
+	want := &LogoutTokenClaims{
+		Issuer:     "zitadel",
+		Subject:    "hello@me.com",
+		Audience:   Audience{"foo", "just@me.com"},
+		Expiration: 12345,
+		JWTID:      "jwtID",
+		Events: map[string]any{
+			"http://schemas.openid.net/event/backchannel-logout": struct{}{},
+		},
+		SessionID: "sessionID",
+		Claims:    nil,
+	}
+
+	got := NewLogoutTokenClaims(
+		want.Issuer,
+		want.Subject,
+		want.Audience,
+		want.Expiration.AsTime(),
+		want.JWTID,
+		want.SessionID,
+		1*time.Second,
+	)
+
+	// test if the dynamic timestamp is around now,
+	// allowing for a delta of 1, just in case we flip on
+	// either side of a second boundry.
+	nowMinusSkew := NowTime() - 1
+	assert.InDelta(t, int64(nowMinusSkew), int64(got.IssuedAt), 1)
+
+	// Make equal not fail on dynamic timestamp
+	got.IssuedAt = 0
+
+	assert.Equal(t, want, got)
+}
diff --git a/pkg/op/config.go b/pkg/op/config.go
index 9fec7cc..2fcede0 100644
--- a/pkg/op/config.go
+++ b/pkg/op/config.go
@@ -49,6 +49,9 @@ type Configuration interface {
 
 	SupportedUILocales() []language.Tag
 	DeviceAuthorization() DeviceAuthorizationConfig
+
+	BackChannelLogoutSupported() bool
+	BackChannelLogoutSessionSupported() bool
 }
 
 type IssuerFromRequest func(r *http.Request) string
diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go
index cd08580..5a79a09 100644
--- a/pkg/op/discovery.go
+++ b/pkg/op/discovery.go
@@ -61,6 +61,8 @@ func CreateDiscoveryConfig(ctx context.Context, config Configuration, storage Di
 		CodeChallengeMethodsSupported:                      CodeChallengeMethods(config),
 		UILocalesSupported:                                 config.SupportedUILocales(),
 		RequestParameterSupported:                          config.RequestObjectSupported(),
+		BackChannelLogoutSupported:                         config.BackChannelLogoutSupported(),
+		BackChannelLogoutSessionSupported:                  config.BackChannelLogoutSessionSupported(),
 	}
 }
 
@@ -92,6 +94,8 @@ func createDiscoveryConfigV2(ctx context.Context, config Configuration, storage
 		CodeChallengeMethodsSupported:                      CodeChallengeMethods(config),
 		UILocalesSupported:                                 config.SupportedUILocales(),
 		RequestParameterSupported:                          config.RequestObjectSupported(),
+		BackChannelLogoutSupported:                         config.BackChannelLogoutSupported(),
+		BackChannelLogoutSessionSupported:                  config.BackChannelLogoutSessionSupported(),
 	}
 }
 
diff --git a/pkg/op/mock/configuration.mock.go b/pkg/op/mock/configuration.mock.go
index f392a45..137c09d 100644
--- a/pkg/op/mock/configuration.mock.go
+++ b/pkg/op/mock/configuration.mock.go
@@ -78,6 +78,34 @@ func (mr *MockConfigurationMockRecorder) AuthorizationEndpoint() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthorizationEndpoint", reflect.TypeOf((*MockConfiguration)(nil).AuthorizationEndpoint))
 }
 
+// BackChannelLogoutSessionSupported mocks base method.
+func (m *MockConfiguration) BackChannelLogoutSessionSupported() bool {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "BackChannelLogoutSessionSupported")
+	ret0, _ := ret[0].(bool)
+	return ret0
+}
+
+// BackChannelLogoutSessionSupported indicates an expected call of BackChannelLogoutSessionSupported.
+func (mr *MockConfigurationMockRecorder) BackChannelLogoutSessionSupported() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BackChannelLogoutSessionSupported", reflect.TypeOf((*MockConfiguration)(nil).BackChannelLogoutSessionSupported))
+}
+
+// BackChannelLogoutSupported mocks base method.
+func (m *MockConfiguration) BackChannelLogoutSupported() bool {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "BackChannelLogoutSupported")
+	ret0, _ := ret[0].(bool)
+	return ret0
+}
+
+// BackChannelLogoutSupported indicates an expected call of BackChannelLogoutSupported.
+func (mr *MockConfigurationMockRecorder) BackChannelLogoutSupported() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BackChannelLogoutSupported", reflect.TypeOf((*MockConfiguration)(nil).BackChannelLogoutSupported))
+}
+
 // CodeMethodS256Supported mocks base method.
 func (m *MockConfiguration) CodeMethodS256Supported() bool {
 	m.ctrl.T.Helper()
diff --git a/pkg/op/op.go b/pkg/op/op.go
index 61c2449..2248098 100644
--- a/pkg/op/op.go
+++ b/pkg/op/op.go
@@ -158,16 +158,18 @@ func authCallbackPath(o OpenIDProvider) string {
 }
 
 type Config struct {
-	CryptoKey                [32]byte
-	DefaultLogoutRedirectURI string
-	CodeMethodS256           bool
-	AuthMethodPost           bool
-	AuthMethodPrivateKeyJWT  bool
-	GrantTypeRefreshToken    bool
-	RequestObjectSupported   bool
-	SupportedUILocales       []language.Tag
-	SupportedClaims          []string
-	DeviceAuthorization      DeviceAuthorizationConfig
+	CryptoKey                         [32]byte
+	DefaultLogoutRedirectURI          string
+	CodeMethodS256                    bool
+	AuthMethodPost                    bool
+	AuthMethodPrivateKeyJWT           bool
+	GrantTypeRefreshToken             bool
+	RequestObjectSupported            bool
+	SupportedUILocales                []language.Tag
+	SupportedClaims                   []string
+	DeviceAuthorization               DeviceAuthorizationConfig
+	BackChannelLogoutSupported        bool
+	BackChannelLogoutSessionSupported bool
 }
 
 // Endpoints defines endpoint routes.
@@ -411,6 +413,14 @@ func (o *Provider) DeviceAuthorization() DeviceAuthorizationConfig {
 	return o.config.DeviceAuthorization
 }
 
+func (o *Provider) BackChannelLogoutSupported() bool {
+	return o.config.BackChannelLogoutSupported
+}
+
+func (o *Provider) BackChannelLogoutSessionSupported() bool {
+	return o.config.BackChannelLogoutSessionSupported
+}
+
 func (o *Provider) Storage() Storage {
 	return o.storage
 }

From fbf009fe75dac732dde39e0eb6fe324b337675e0 Mon Sep 17 00:00:00 2001
From: David Sharnoff 
Date: Fri, 1 Nov 2024 01:53:28 -0700
Subject: [PATCH 439/502] fix: ignore all unmarshal errors from locale (#673)

---
 pkg/oidc/types.go      | 15 ++++-----------
 pkg/oidc/types_test.go |  8 +++++---
 2 files changed, 9 insertions(+), 14 deletions(-)

diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go
index e7292e6..7063426 100644
--- a/pkg/oidc/types.go
+++ b/pkg/oidc/types.go
@@ -3,7 +3,6 @@ package oidc
 import (
 	"database/sql/driver"
 	"encoding/json"
-	"errors"
 	"fmt"
 	"reflect"
 	"strings"
@@ -78,22 +77,16 @@ func (l *Locale) MarshalJSON() ([]byte, error) {
 }
 
 // UnmarshalJSON implements json.Unmarshaler.
-// When [language.ValueError] is encountered, the containing tag will be set
+// All unmarshal errors for are ignored.
+// When an error is encountered, the containing tag will be set
 // to an empty value (language "und") and no error will be returned.
 // This state can be checked with the `l.Tag().IsRoot()` method.
 func (l *Locale) UnmarshalJSON(data []byte) error {
 	err := json.Unmarshal(data, &l.tag)
-	if err == nil {
-		return nil
-	}
-
-	// catch "well-formed but unknown" errors
-	var target language.ValueError
-	if errors.As(err, &target) {
+	if err != nil {
 		l.tag = language.Tag{}
-		return nil
 	}
-	return err
+	return nil
 }
 
 type Locales []language.Tag
diff --git a/pkg/oidc/types_test.go b/pkg/oidc/types_test.go
index df93a73..c7ce0ee 100644
--- a/pkg/oidc/types_test.go
+++ b/pkg/oidc/types_test.go
@@ -232,9 +232,11 @@ func TestLocale_UnmarshalJSON(t *testing.T) {
 			},
 		},
 		{
-			name:    "bad form, error",
-			input:   `{"locale": "g!!!!!"}`,
-			wantErr: true,
+			name:  "bad form, error",
+			input: `{"locale": "g!!!!!"}`,
+			want: dst{
+				Locale: &Locale{},
+			},
 		},
 	}
 

From f194951e6194c49f643106c2cc970edbdc9e99ea Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 11 Nov 2024 12:52:23 +0200
Subject: [PATCH 440/502] chore(deps): bump golang.org/x/text from 0.19.0 to
 0.20.0 (#677)

Bumps [golang.org/x/text](https://github.com/golang/text) from 0.19.0 to 0.20.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.19.0...v0.20.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 go.mod | 2 +-
 go.sum | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/go.mod b/go.mod
index f50972a..a898848 100644
--- a/go.mod
+++ b/go.mod
@@ -20,7 +20,7 @@ require (
 	github.com/zitadel/schema v1.3.0
 	go.opentelemetry.io/otel v1.29.0
 	golang.org/x/oauth2 v0.23.0
-	golang.org/x/text v0.19.0
+	golang.org/x/text v0.20.0
 )
 
 require (
diff --git a/go.sum b/go.sum
index 91de9bf..8868b30 100644
--- a/go.sum
+++ b/go.sum
@@ -88,8 +88,8 @@ golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
-golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
+golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=

From 87ab01115708a2a05c20cfd26db82cc7e0e8e338 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 11 Nov 2024 12:55:25 +0200
Subject: [PATCH 441/502] chore(deps): bump golang.org/x/oauth2 from 0.23.0 to
 0.24.0 (#676)

Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.23.0 to 0.24.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.23.0...v0.24.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 go.mod | 2 +-
 go.sum | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/go.mod b/go.mod
index a898848..82e15e1 100644
--- a/go.mod
+++ b/go.mod
@@ -19,7 +19,7 @@ require (
 	github.com/zitadel/logging v0.6.1
 	github.com/zitadel/schema v1.3.0
 	go.opentelemetry.io/otel v1.29.0
-	golang.org/x/oauth2 v0.23.0
+	golang.org/x/oauth2 v0.24.0
 	golang.org/x/text v0.20.0
 )
 
diff --git a/go.sum b/go.sum
index 8868b30..2f64061 100644
--- a/go.sum
+++ b/go.sum
@@ -73,8 +73,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
 golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
 golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
-golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
+golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

From 8afb8b8d5fb036b2688b773596d5dd992ba63cf5 Mon Sep 17 00:00:00 2001
From: Kevin Schoonover 
Date: Tue, 12 Nov 2024 07:06:24 -0800
Subject: [PATCH 442/502] feat(pkg/op): allow custom SupportedScopes (#675)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: Tim Möhlmann 
---
 pkg/op/discovery.go      | 8 ++++++--
 pkg/op/discovery_test.go | 5 +++++
 pkg/op/op.go             | 1 +
 3 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go
index 5a79a09..e30a5a4 100644
--- a/pkg/op/discovery.go
+++ b/pkg/op/discovery.go
@@ -100,7 +100,11 @@ func createDiscoveryConfigV2(ctx context.Context, config Configuration, storage
 }
 
 func Scopes(c Configuration) []string {
-	return DefaultSupportedScopes // TODO: config
+	provider, ok := c.(*Provider)
+	if ok && provider.config.SupportedScopes != nil {
+		return provider.config.SupportedScopes
+	}
+	return DefaultSupportedScopes
 }
 
 func ResponseTypes(c Configuration) []string {
@@ -135,7 +139,7 @@ func GrantTypes(c Configuration) []oidc.GrantType {
 }
 
 func SubjectTypes(c Configuration) []string {
-	return []string{"public"} //TODO: config
+	return []string{"public"} // TODO: config
 }
 
 func SigAlgorithms(ctx context.Context, storage DiscoverStorage) []string {
diff --git a/pkg/op/discovery_test.go b/pkg/op/discovery_test.go
index cb4cfba..61afb62 100644
--- a/pkg/op/discovery_test.go
+++ b/pkg/op/discovery_test.go
@@ -81,6 +81,11 @@ func Test_scopes(t *testing.T) {
 			args{},
 			op.DefaultSupportedScopes,
 		},
+		{
+			"custom scopes",
+			args{newTestProvider(&op.Config{SupportedScopes: []string{"test1", "test2"}})},
+			[]string{"test1", "test2"},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/op/op.go b/pkg/op/op.go
index 2248098..190c2c4 100644
--- a/pkg/op/op.go
+++ b/pkg/op/op.go
@@ -167,6 +167,7 @@ type Config struct {
 	RequestObjectSupported            bool
 	SupportedUILocales                []language.Tag
 	SupportedClaims                   []string
+	SupportedScopes                   []string
 	DeviceAuthorization               DeviceAuthorizationConfig
 	BackChannelLogoutSupported        bool
 	BackChannelLogoutSessionSupported bool

From 897c720070c0cca82f8b898b5f8db53c73f54881 Mon Sep 17 00:00:00 2001
From: isegura-eos-eng <77284860+isegura-eos-eng@users.noreply.github.com>
Date: Wed, 13 Nov 2024 09:49:55 +0100
Subject: [PATCH 443/502] fix(op): add scope to access token scope (#664)

---
 pkg/oidc/token.go                  | 13 +++++++------
 pkg/op/device.go                   |  1 +
 pkg/op/op_test.go                  |  2 +-
 pkg/op/server_http_routes_test.go  |  4 ++--
 pkg/op/token.go                    |  1 +
 pkg/op/token_client_credentials.go |  1 +
 pkg/op/token_jwt_profile.go        |  1 +
 7 files changed, 14 insertions(+), 9 deletions(-)

diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go
index e57d91e..d2b6f6d 100644
--- a/pkg/oidc/token.go
+++ b/pkg/oidc/token.go
@@ -230,12 +230,13 @@ func (c *ActorClaims) UnmarshalJSON(data []byte) error {
 }
 
 type AccessTokenResponse struct {
-	AccessToken  string `json:"access_token,omitempty" schema:"access_token,omitempty"`
-	TokenType    string `json:"token_type,omitempty" schema:"token_type,omitempty"`
-	RefreshToken string `json:"refresh_token,omitempty" schema:"refresh_token,omitempty"`
-	ExpiresIn    uint64 `json:"expires_in,omitempty" schema:"expires_in,omitempty"`
-	IDToken      string `json:"id_token,omitempty" schema:"id_token,omitempty"`
-	State        string `json:"state,omitempty" schema:"state,omitempty"`
+	AccessToken  string              `json:"access_token,omitempty" schema:"access_token,omitempty"`
+	TokenType    string              `json:"token_type,omitempty" schema:"token_type,omitempty"`
+	RefreshToken string              `json:"refresh_token,omitempty" schema:"refresh_token,omitempty"`
+	ExpiresIn    uint64              `json:"expires_in,omitempty" schema:"expires_in,omitempty"`
+	IDToken      string              `json:"id_token,omitempty" schema:"id_token,omitempty"`
+	State        string              `json:"state,omitempty" schema:"state,omitempty"`
+	Scope        SpaceDelimitedArray `json:"scope,omitempty" schema:"scope,omitempty"`
 }
 
 type JWTProfileAssertionClaims struct {
diff --git a/pkg/op/device.go b/pkg/op/device.go
index 11638b0..3de271a 100644
--- a/pkg/op/device.go
+++ b/pkg/op/device.go
@@ -344,6 +344,7 @@ func CreateDeviceTokenResponse(ctx context.Context, tokenRequest TokenRequest, c
 		RefreshToken: refreshToken,
 		TokenType:    oidc.BearerToken,
 		ExpiresIn:    uint64(validity.Seconds()),
+		Scope:        tokenRequest.GetScopes(),
 	}
 
 	// TODO(v4): remove type assertion
diff --git a/pkg/op/op_test.go b/pkg/op/op_test.go
index 83032d4..9a4a624 100644
--- a/pkg/op/op_test.go
+++ b/pkg/op/op_test.go
@@ -232,7 +232,7 @@ func TestRoutes(t *testing.T) {
 				"scope":      oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.String(),
 			},
 			wantCode: http.StatusOK,
-			contains: []string{`{"access_token":"`, `","token_type":"Bearer","expires_in":299}`},
+			contains: []string{`{"access_token":"`, `","token_type":"Bearer","expires_in":299,"scope":"openid offline_access"}`},
 		},
 		{
 			// This call will fail. A successful test is already
diff --git a/pkg/op/server_http_routes_test.go b/pkg/op/server_http_routes_test.go
index 2c83ad3..1bfb32b 100644
--- a/pkg/op/server_http_routes_test.go
+++ b/pkg/op/server_http_routes_test.go
@@ -145,7 +145,7 @@ func TestServerRoutes(t *testing.T) {
 				"assertion":  jwtProfileToken,
 			},
 			wantCode: http.StatusOK,
-			contains: []string{`{"access_token":`, `"token_type":"Bearer","expires_in":299}`},
+			contains: []string{`{"access_token":`, `"token_type":"Bearer","expires_in":299,"scope":"openid"}`},
 		},
 		{
 			name:      "Token exchange",
@@ -174,7 +174,7 @@ func TestServerRoutes(t *testing.T) {
 				"scope":      oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.String(),
 			},
 			wantCode: http.StatusOK,
-			contains: []string{`{"access_token":"`, `","token_type":"Bearer","expires_in":299}`},
+			contains: []string{`{"access_token":"`, `","token_type":"Bearer","expires_in":299,"scope":"openid offline_access"}`},
 		},
 		{
 			// This call will fail. A successful test is already
diff --git a/pkg/op/token.go b/pkg/op/token.go
index b45789b..61d7b2f 100644
--- a/pkg/op/token.go
+++ b/pkg/op/token.go
@@ -65,6 +65,7 @@ func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Cli
 		TokenType:    oidc.BearerToken,
 		ExpiresIn:    exp,
 		State:        state,
+		Scope:        request.GetScopes(),
 	}, nil
 }
 
diff --git a/pkg/op/token_client_credentials.go b/pkg/op/token_client_credentials.go
index 7f1debe..63dcc79 100644
--- a/pkg/op/token_client_credentials.go
+++ b/pkg/op/token_client_credentials.go
@@ -120,5 +120,6 @@ func CreateClientCredentialsTokenResponse(ctx context.Context, tokenRequest Toke
 		AccessToken: accessToken,
 		TokenType:   oidc.BearerToken,
 		ExpiresIn:   uint64(validity.Seconds()),
+		Scope:       tokenRequest.GetScopes(),
 	}, nil
 }
diff --git a/pkg/op/token_jwt_profile.go b/pkg/op/token_jwt_profile.go
index 96ce1ed..d1a7ff5 100644
--- a/pkg/op/token_jwt_profile.go
+++ b/pkg/op/token_jwt_profile.go
@@ -89,6 +89,7 @@ func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, crea
 		AccessToken: accessToken,
 		TokenType:   oidc.BearerToken,
 		ExpiresIn:   uint64(validity.Seconds()),
+		Scope:       tokenRequest.GetScopes(),
 	}, nil
 }
 

From 1464268851631e7d3062438fe79206e32a35ed83 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= 
Date: Fri, 15 Nov 2024 08:26:03 +0200
Subject: [PATCH 444/502] chore(deps): upgrade go to v1.23 (#681)

---
 .github/workflows/release.yml |  2 +-
 README.md                     | 72 ++++++++++++++++++-----------------
 2 files changed, 39 insertions(+), 35 deletions(-)

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 66d68b6..a1fb1ba 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -18,7 +18,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        go: ['1.21', '1.22']
+        go: ['1.21', '1.22', '1.23']
     name: Go ${{ matrix.go }} test
     steps:
       - uses: actions/checkout@v4
diff --git a/README.md b/README.md
index b102815..04d551f 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,7 @@ Whenever possible we tried to reuse / extend existing packages like `OAuth2 for
 ## Basic Overview
 
 The most important packages of the library:
+
 
 /pkg
     /client            clients using the OP for retrieving, exchanging and verifying tokens
@@ -37,7 +38,6 @@ The most important packages of the library:
     /server            examples of an OpenID Provider implementations (including dynamic) with some very basic login UI
 
- ### Semver This package uses [semver](https://semver.org/) for [releases](https://github.com/zitadel/oidc/releases). Major releases ship breaking changes. Starting with the `v2` to `v3` increment we provide an [upgrade guide](UPGRADING.md) to ease migration to a newer version. @@ -60,10 +60,13 @@ CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998/ SCOPES="openid - the OP will redirect you to the client app, which displays the user info for the dynamic issuer, just start it with: + ```bash go run github.com/zitadel/oidc/v3/example/server/dynamic ``` + the oidc web client above will still work, but if you add `oidc.local` (pointing to 127.0.0.1) in your hosts file you can also start it with: + ```bash CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v3/example/client/app ``` @@ -75,35 +78,36 @@ CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid Example server allows extra configuration using environment variables and could be used for end to end testing of your services. -| Name | Format | Description | -|---------------|--------------------------------------|---------------------------------------| -| PORT | Number between 1 and 65535 | OIDC listen port | -| REDIRECT_URI | Comma-separated URIs | List of allowed redirect URIs | -| USERS_FILE | Path to json in local filesystem | Users with their data and credentials | +| Name | Format | Description | +| ------------ | -------------------------------- | ------------------------------------- | +| PORT | Number between 1 and 65535 | OIDC listen port | +| REDIRECT_URI | Comma-separated URIs | List of allowed redirect URIs | +| USERS_FILE | Path to json in local filesystem | Users with their data and credentials | Here is json equivalent for one of the default users + ```json { - "id2": { - "ID": "id2", - "Username": "test-user2", - "Password": "verysecure", - "FirstName": "Test", - "LastName": "User2", - "Email": "test-user2@zitadel.ch", - "EmailVerified": true, - "Phone": "", - "PhoneVerified": false, - "PreferredLanguage": "DE", - "IsAdmin": false - } + "id2": { + "ID": "id2", + "Username": "test-user2", + "Password": "verysecure", + "FirstName": "Test", + "LastName": "User2", + "Email": "test-user2@zitadel.ch", + "EmailVerified": true, + "Phone": "", + "PhoneVerified": false, + "PreferredLanguage": "DE", + "IsAdmin": false + } } ``` ## Features | | Relying party | OpenID Provider | Specification | -|----------------------| ------------- | --------------- |----------------------------------------------| +| -------------------- | ------------- | --------------- | -------------------------------------------- | | Code Flow | yes | yes | OpenID Connect Core 1.0, [Section 3.1][1] | | Implicit Flow | no[^1] | yes | OpenID Connect Core 1.0, [Section 3.2][2] | | Hybrid Flow | no | not yet | OpenID Connect Core 1.0, [Section 3.3][3] | @@ -117,18 +121,18 @@ Here is json equivalent for one of the default users | mTLS | not yet | not yet | [RFC 8705][11] | | Back-Channel Logout | not yet | yes | OpenID Connect [Back-Channel Logout][12] 1.0 | -[1]: "3.1. Authentication using the Authorization Code Flow" -[2]: "3.2. Authentication using the Implicit Flow" -[3]: "3.3. Authentication using the Hybrid Flow" -[4]: "9. Client Authentication" -[5]: "12. Using Refresh Tokens" -[6]: "OpenID Connect Discovery 1.0 incorporating errata set 1" -[7]: "JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants" -[8]: "Proof Key for Code Exchange by OAuth Public Clients" -[9]: "OAuth 2.0 Token Exchange" -[10]: "OAuth 2.0 Device Authorization Grant" -[11]: "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens" -[12]: "OpenID Connect Back-Channel Logout 1.0 incorporating errata set 1" +[1]: https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth "3.1. Authentication using the Authorization Code Flow" +[2]: https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth "3.2. Authentication using the Implicit Flow" +[3]: https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth "3.3. Authentication using the Hybrid Flow" +[4]: https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication "9. Client Authentication" +[5]: https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens "12. Using Refresh Tokens" +[6]: https://openid.net/specs/openid-connect-discovery-1_0.html "OpenID Connect Discovery 1.0 incorporating errata set 1" +[7]: https://www.rfc-editor.org/rfc/rfc7523.html "JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants" +[8]: https://www.rfc-editor.org/rfc/rfc7636.html "Proof Key for Code Exchange by OAuth Public Clients" +[9]: https://www.rfc-editor.org/rfc/rfc8693.html "OAuth 2.0 Token Exchange" +[10]: https://www.rfc-editor.org/rfc/rfc8628.html "OAuth 2.0 Device Authorization Grant" +[11]: https://www.rfc-editor.org/rfc/rfc8705.html "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens" +[12]: https://openid.net/specs/openid-connect-backchannel-1_0.html "OpenID Connect Back-Channel Logout 1.0 incorporating errata set 1" ## Contributors @@ -153,8 +157,9 @@ Versions that also build are marked with :warning:. | Version | Supported | | ------- | ------------------ | | <1.21 | :x: | -| 1.21 | :white_check_mark: | +| 1.21 | :warning: | | 1.22 | :white_check_mark: | +| 1.23 | :white_check_mark: | ## Why another library @@ -185,5 +190,4 @@ Unless required by applicable law or agreed to in writing, software distributed AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - [^1]: https://github.com/zitadel/oidc/issues/135#issuecomment-950563892 From 6d2092802811f0a9d38cc0a070f2fff708a44181 Mon Sep 17 00:00:00 2001 From: isegura-eos-eng <77284860+isegura-eos-eng@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:47:32 +0100 Subject: [PATCH 445/502] refactor: mark pkg/strings as deprecated in favor of stdlib (#680) * refactor: mark pkg/strings as deprecated in favor of stdlib * format: reword deprecate notice and use doc links --- pkg/oidc/verifier.go | 7 +++---- pkg/op/auth_request.go | 7 +++---- pkg/op/device.go | 6 +++--- pkg/op/token.go | 6 +++--- pkg/op/token_refresh.go | 4 ++-- pkg/strings/strings.go | 11 +++++------ 6 files changed, 19 insertions(+), 22 deletions(-) diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go index f580da6..d5e0213 100644 --- a/pkg/oidc/verifier.go +++ b/pkg/oidc/verifier.go @@ -7,12 +7,11 @@ import ( "encoding/json" "errors" "fmt" + "slices" "strings" "time" jose "github.com/go-jose/go-jose/v4" - - str "github.com/zitadel/oidc/v3/pkg/strings" ) type Claims interface { @@ -84,7 +83,7 @@ type ACRVerifier func(string) error // if none of the provided values matches the acr claim func DefaultACRVerifier(possibleValues []string) ACRVerifier { return func(acr string) error { - if !str.Contains(possibleValues, acr) { + if !slices.Contains(possibleValues, acr) { return fmt.Errorf("expected one of: %v, got: %q", possibleValues, acr) } return nil @@ -123,7 +122,7 @@ func CheckIssuer(claims Claims, issuer string) error { } func CheckAudience(claims Claims, clientID string) error { - if !str.Contains(claims.GetAudience(), clientID) { + if !slices.Contains(claims.GetAudience(), clientID) { return fmt.Errorf("%w: Audience must contain client_id %q", ErrAudience, clientID) } diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 52fda2e..b020f39 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -18,7 +18,6 @@ import ( "github.com/bmatcuk/doublestar/v4" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" - str "github.com/zitadel/oidc/v3/pkg/strings" ) type AuthRequest interface { @@ -156,7 +155,7 @@ func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage if requestObject.Issuer != requestObject.ClientID { return oidc.ErrInvalidRequest().WithDescription("missing or wrong issuer in request") } - if !str.Contains(requestObject.Audience, issuer) { + if !slices.Contains(requestObject.Audience, issuer) { return oidc.ErrInvalidRequest().WithDescription("issuer missing in audience") } keySet := &jwtProfileKeySet{storage: storage, clientID: requestObject.Issuer} @@ -170,7 +169,7 @@ func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage // CopyRequestObjectToAuthRequest overwrites present values from the Request Object into the auth request // and clears the `RequestParam` of the auth request func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oidc.RequestObject) { - if str.Contains(authReq.Scopes, oidc.ScopeOpenID) && len(requestObject.Scopes) > 0 { + if slices.Contains(authReq.Scopes, oidc.ScopeOpenID) && len(requestObject.Scopes) > 0 { authReq.Scopes = requestObject.Scopes } if requestObject.RedirectURI != "" { @@ -288,7 +287,7 @@ func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) { // checkURIAgainstRedirects just checks aginst the valid redirect URIs and ignores // other factors. func checkURIAgainstRedirects(client Client, uri string) error { - if str.Contains(client.RedirectURIs(), uri) { + if slices.Contains(client.RedirectURIs(), uri) { return nil } if globClient, ok := client.(HasRedirectGlobs); ok { diff --git a/pkg/op/device.go b/pkg/op/device.go index 3de271a..8a0e174 100644 --- a/pkg/op/device.go +++ b/pkg/op/device.go @@ -9,12 +9,12 @@ import ( "math/big" "net/http" "net/url" + "slices" "strings" "time" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" - strs "github.com/zitadel/oidc/v3/pkg/strings" ) type DeviceAuthorizationConfig struct { @@ -276,7 +276,7 @@ func (r *DeviceAuthorizationState) GetAMR() []string { } func (r *DeviceAuthorizationState) GetAudience() []string { - if !strs.Contains(r.Audience, r.ClientID) { + if !slices.Contains(r.Audience, r.ClientID) { r.Audience = append(r.Audience, r.ClientID) } return r.Audience @@ -348,7 +348,7 @@ func CreateDeviceTokenResponse(ctx context.Context, tokenRequest TokenRequest, c } // TODO(v4): remove type assertion - if idTokenRequest, ok := tokenRequest.(IDTokenRequest); ok && strs.Contains(tokenRequest.GetScopes(), oidc.ScopeOpenID) { + if idTokenRequest, ok := tokenRequest.(IDTokenRequest); ok && slices.Contains(tokenRequest.GetScopes(), oidc.ScopeOpenID) { response.IDToken, err = CreateIDToken(ctx, IssuerFromContext(ctx), idTokenRequest, client.IDTokenLifetime(), accessToken, "", creator.Storage(), client) if err != nil { return nil, err diff --git a/pkg/op/token.go b/pkg/op/token.go index 61d7b2f..04cd3cc 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -2,11 +2,11 @@ package op import ( "context" + "slices" "time" "github.com/zitadel/oidc/v3/pkg/crypto" "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/strings" ) type TokenCreator interface { @@ -83,13 +83,13 @@ func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storag func needsRefreshToken(tokenRequest TokenRequest, client AccessTokenClient) bool { switch req := tokenRequest.(type) { case AuthRequest: - return strings.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && req.GetResponseType() == oidc.ResponseTypeCode && ValidateGrantType(client, oidc.GrantTypeRefreshToken) + return slices.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && req.GetResponseType() == oidc.ResponseTypeCode && ValidateGrantType(client, oidc.GrantTypeRefreshToken) case TokenExchangeRequest: return req.GetRequestedTokenType() == oidc.RefreshTokenType case RefreshTokenRequest: return true case *DeviceAuthorizationState: - return strings.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && ValidateGrantType(client, oidc.GrantTypeRefreshToken) + return slices.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && ValidateGrantType(client, oidc.GrantTypeRefreshToken) default: return false } diff --git a/pkg/op/token_refresh.go b/pkg/op/token_refresh.go index 92ef476..7c8c1c0 100644 --- a/pkg/op/token_refresh.go +++ b/pkg/op/token_refresh.go @@ -4,11 +4,11 @@ import ( "context" "errors" "net/http" + "slices" "time" httphelper "github.com/zitadel/oidc/v3/pkg/http" "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/strings" ) type RefreshTokenRequest interface { @@ -85,7 +85,7 @@ func ValidateRefreshTokenScopes(requestedScopes []string, authRequest RefreshTok return nil } for _, scope := range requestedScopes { - if !strings.Contains(authRequest.GetScopes(), scope) { + if !slices.Contains(authRequest.GetScopes(), scope) { return oidc.ErrInvalidScope() } } diff --git a/pkg/strings/strings.go b/pkg/strings/strings.go index af48cf3..b8f43a1 100644 --- a/pkg/strings/strings.go +++ b/pkg/strings/strings.go @@ -1,10 +1,9 @@ package strings +import "slices" + +// Deprecated: Use standard library [slices.Contains] instead. func Contains(list []string, needle string) bool { - for _, item := range list { - if item == needle { - return true - } - } - return false + // TODO(v4): remove package. + return slices.Contains(list, needle) } From a7833f828c2eafa2d5bc69136c945f85edda6c2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 12:59:21 +0200 Subject: [PATCH 446/502] chore(deps): bump codecov/codecov-action from 4.6.0 to 5.0.2 (#682) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.6.0 to 5.0.2. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4.6.0...v5.0.2) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a1fb1ba..8cc5896 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v4.6.0 + - uses: codecov/codecov-action@v5.0.2 with: file: ./profile.cov name: codecov-go From e2de68a7dd74797a92da0010ff8cbde807fe8c97 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 10:04:43 +0200 Subject: [PATCH 447/502] chore(deps): bump github.com/jeremija/gosubmit from 0.2.7 to 0.2.8 (#683) Bumps [github.com/jeremija/gosubmit](https://github.com/jeremija/gosubmit) from 0.2.7 to 0.2.8. - [Commits](https://github.com/jeremija/gosubmit/compare/v0.2.7...v0.2.8) --- updated-dependencies: - dependency-name: github.com/jeremija/gosubmit dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 82e15e1..69aa537 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/google/go-github/v31 v31.0.0 github.com/google/uuid v1.6.0 github.com/gorilla/securecookie v1.1.2 - github.com/jeremija/gosubmit v0.2.7 + github.com/jeremija/gosubmit v0.2.8 github.com/muhlemmer/gu v0.3.1 github.com/muhlemmer/httpforwarded v0.1.0 github.com/rs/cors v1.11.1 diff --git a/go.sum b/go.sum index 2f64061..eda85e5 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= -github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc= -github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= +github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA= +github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= From 67bd2f572032185f0c77751f26df5a0900597909 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 10:55:33 +0200 Subject: [PATCH 448/502] chore(deps): bump github.com/stretchr/testify from 1.9.0 to 1.10.0 (#684) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.9.0 to 1.10.0. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.9.0...v1.10.0) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 69aa537..9be2bce 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/muhlemmer/httpforwarded v0.1.0 github.com/rs/cors v1.11.1 github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/zitadel/logging v0.6.1 github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.29.0 diff --git a/go.sum b/go.sum index eda85e5..c76d047 100644 --- a/go.sum +++ b/go.sum @@ -47,8 +47,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/zitadel/logging v0.6.1 h1:Vyzk1rl9Kq9RCevcpX6ujUaTYFX43aa4LkvV1TvUk+Y= github.com/zitadel/logging v0.6.1/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= From 057601ff3f65a9db73b2c10840fd11b97a2e3c85 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 11:41:27 +0200 Subject: [PATCH 449/502] chore(deps): bump codecov/codecov-action from 5.0.2 to 5.0.7 (#685) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.0.2 to 5.0.7. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v5.0.2...v5.0.7) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8cc5896..cfcada3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v5.0.2 + - uses: codecov/codecov-action@v5.0.7 with: file: ./profile.cov name: codecov-go From 2513e21531093a5648957801fdbef5102907f586 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:42:45 +0100 Subject: [PATCH 450/502] chore(deps): bump golang.org/x/text from 0.20.0 to 0.21.0 (#686) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.20.0 to 0.21.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.20.0...v0.21.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9be2bce..1ad4399 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.29.0 golang.org/x/oauth2 v0.24.0 - golang.org/x/text v0.20.0 + golang.org/x/text v0.21.0 ) require ( diff --git a/go.sum b/go.sum index c76d047..d0281cf 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,8 @@ golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= From cf6ce69d79666cef2ac429607286c3aa63bcb677 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 14:16:13 +0100 Subject: [PATCH 451/502] chore(deps): bump codecov/codecov-action from 5.0.7 to 5.1.1 (#687) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.0.7 to 5.1.1. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v5.0.7...v5.1.1) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cfcada3..2d013be 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v5.0.7 + - uses: codecov/codecov-action@v5.1.1 with: file: ./profile.cov name: codecov-go From 9a93b7c70d06e554d68050cba33b8a7b5bd534bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:33:24 +0000 Subject: [PATCH 452/502] chore(deps): bump golang.org/x/crypto from 0.25.0 to 0.31.0 (#688) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.25.0 to 0.31.0. - [Commits](https://github.com/golang/crypto/compare/v0.25.0...v0.31.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 1ad4399..666422f 100644 --- a/go.mod +++ b/go.mod @@ -31,8 +31,8 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/otel/metric v1.29.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect - golang.org/x/crypto v0.25.0 // indirect + golang.org/x/crypto v0.31.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.22.0 // indirect + golang.org/x/sys v0.28.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d0281cf..827565f 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt3 go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -83,8 +83,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From b36a8e2ec14845c33c368c3d178c077d5fc7e7b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:27:45 +0200 Subject: [PATCH 453/502] chore(deps): bump github.com/go-chi/chi/v5 from 5.1.0 to 5.2.0 (#689) Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.1.0 to 5.2.0. - [Release notes](https://github.com/go-chi/chi/releases) - [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md) - [Commits](https://github.com/go-chi/chi/compare/v5.1.0...v5.2.0) --- updated-dependencies: - dependency-name: github.com/go-chi/chi/v5 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 666422f..25b2eac 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/bmatcuk/doublestar/v4 v4.7.1 - github.com/go-chi/chi/v5 v5.1.0 + github.com/go-chi/chi/v5 v5.2.0 github.com/go-jose/go-jose/v4 v4.0.4 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 diff --git a/go.sum b/go.sum index 827565f..2927da3 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTS 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/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= -github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= +github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= From 6c90652dfb1f1dbd930283a4c4caef255ff7b406 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Dec 2024 11:00:57 +0200 Subject: [PATCH 454/502] chore(deps): bump codecov/codecov-action from 5.1.1 to 5.1.2 (#692) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.1.1 to 5.1.2. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v5.1.1...v5.1.2) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2d013be..f70bd8b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v5.1.1 + - uses: codecov/codecov-action@v5.1.2 with: file: ./profile.cov name: codecov-go From 8d971dcad8f6e7aab12e4494de84bf79ecd521c0 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Mon, 30 Dec 2024 11:47:05 +0100 Subject: [PATCH 455/502] chore: bump dependencies (#694) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 25b2eac..7f38f6d 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( go.opentelemetry.io/otel/metric v1.29.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect golang.org/x/crypto v0.31.0 // indirect - golang.org/x/net v0.26.0 // indirect + golang.org/x/net v0.33.0 // indirect golang.org/x/sys v0.28.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2927da3..ec07f36 100644 --- a/go.sum +++ b/go.sum @@ -70,8 +70,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= From a0f67c0b4baacb05cfedd564237bc523ef706452 Mon Sep 17 00:00:00 2001 From: Danila Fominykh Date: Fri, 3 Jan 2025 11:27:01 +0300 Subject: [PATCH 456/502] feat: add redirect URI-s ENV setting to web clients (#693) Co-authored-by: FominykhDG --- example/server/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/server/main.go b/example/server/main.go index 36816d6..6d345e1 100644 --- a/example/server/main.go +++ b/example/server/main.go @@ -32,8 +32,8 @@ func main() { storage.RegisterClients( storage.NativeClient("native", cfg.RedirectURI...), - storage.WebClient("web", "secret"), - storage.WebClient("api", "secret"), + storage.WebClient("web", "secret", cfg.RedirectURI...), + storage.WebClient("api", "secret", cfg.RedirectURI...), ) // the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations From 1f6a0d5d89452a0b97b3e2d0afce4a5bd13c3b20 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 10:47:02 +0200 Subject: [PATCH 457/502] chore(deps): bump golang.org/x/oauth2 from 0.24.0 to 0.25.0 (#695) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.24.0 to 0.25.0. - [Commits](https://github.com/golang/oauth2/compare/v0.24.0...v0.25.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7f38f6d..ff8f4af 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/zitadel/logging v0.6.1 github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.29.0 - golang.org/x/oauth2 v0.24.0 + golang.org/x/oauth2 v0.25.0 golang.org/x/text v0.21.0 ) diff --git a/go.sum b/go.sum index ec07f36..f531586 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= -golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= +golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= From 867a4806fdad92dcf2db83be62f3444da8e17d68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jan 2025 14:51:01 +0100 Subject: [PATCH 458/502] chore(deps): bump github.com/bmatcuk/doublestar/v4 from 4.7.1 to 4.8.0 (#696) Bumps [github.com/bmatcuk/doublestar/v4](https://github.com/bmatcuk/doublestar) from 4.7.1 to 4.8.0. - [Release notes](https://github.com/bmatcuk/doublestar/releases) - [Commits](https://github.com/bmatcuk/doublestar/compare/v4.7.1...v4.8.0) --- updated-dependencies: - dependency-name: github.com/bmatcuk/doublestar/v4 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ff8f4af..47feab9 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/zitadel/oidc/v3 go 1.21 require ( - github.com/bmatcuk/doublestar/v4 v4.7.1 + github.com/bmatcuk/doublestar/v4 v4.8.0 github.com/go-chi/chi/v5 v5.2.0 github.com/go-jose/go-jose/v4 v4.0.4 github.com/golang/mock v1.6.0 diff --git a/go.sum b/go.sum index f531586..60a5125 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q= -github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.8.0 h1:DSXtrypQddoug1459viM9X9D3dp1Z7993fw36I2kNcQ= +github.com/bmatcuk/doublestar/v4 v4.8.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 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= From de2fd41f40f3097734b6b593be3d94dee8b47e2a Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 17 Jan 2025 14:53:19 +0100 Subject: [PATCH 459/502] fix: allow native clients to use https:// on localhost redirects (#691) --- pkg/op/auth_request.go | 21 ++++++++++++--------- pkg/op/auth_request_test.go | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index b020f39..d6db62b 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -312,12 +312,12 @@ func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.Res return oidc.ErrInvalidRequestRedirectURI().WithDescription("The redirect_uri is missing in the request. " + "Please ensure it is added to the request. If you have any questions, you may contact the administrator of the application.") } - if strings.HasPrefix(uri, "https://") { - return checkURIAgainstRedirects(client, uri) - } if client.ApplicationType() == ApplicationTypeNative { return validateAuthReqRedirectURINative(client, uri) } + if strings.HasPrefix(uri, "https://") { + return checkURIAgainstRedirects(client, uri) + } if err := checkURIAgainstRedirects(client, uri); err != nil { return err } @@ -338,12 +338,15 @@ func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.Res // ValidateAuthReqRedirectURINative validates the passed redirect_uri and response_type to the registered uris and client type func validateAuthReqRedirectURINative(client Client, uri string) error { parsedURL, isLoopback := HTTPLoopbackOrLocalhost(uri) - isCustomSchema := !strings.HasPrefix(uri, "http://") + isCustomSchema := !(strings.HasPrefix(uri, "http://") || strings.HasPrefix(uri, "https://")) if err := checkURIAgainstRedirects(client, uri); err == nil { if client.DevMode() { return nil } - // The RedirectURIs are only valid for native clients when localhost or non-"http://" + if !isLoopback && strings.HasPrefix(uri, "https://") { + return nil + } + // The RedirectURIs are only valid for native clients when localhost or non-"http://" and "https://" if isLoopback || isCustomSchema { return nil } @@ -373,11 +376,11 @@ func HTTPLoopbackOrLocalhost(rawURL string) (*url.URL, bool) { if err != nil { return nil, false } - if parsedURL.Scheme != "http" { - return nil, false + if parsedURL.Scheme == "http" || parsedURL.Scheme == "https" { + hostName := parsedURL.Hostname() + return parsedURL, hostName == "localhost" || net.ParseIP(hostName).IsLoopback() } - hostName := parsedURL.Hostname() - return parsedURL, hostName == "localhost" || net.ParseIP(hostName).IsLoopback() + return nil, false } // ValidateAuthReqResponseType validates the passed response_type to the registered response types diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index 6b4af17..765e602 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -433,6 +433,24 @@ func TestValidateAuthReqRedirectURI(t *testing.T) { }, false, }, + { + "code flow registered https loopback v4 native ok", + args{ + "https://127.0.0.1:4200/callback", + mock.NewClientWithConfig(t, []string{"https://127.0.0.1/callback"}, op.ApplicationTypeNative, nil, false), + oidc.ResponseTypeCode, + }, + false, + }, + { + "code flow registered https loopback v6 native ok", + args{ + "https://[::1]:4200/callback", + mock.NewClientWithConfig(t, []string{"https://[::1]/callback"}, op.ApplicationTypeNative, nil, false), + oidc.ResponseTypeCode, + }, + false, + }, { "code flow unregistered http native fails", args{ From 24c96c361d657901566c321e5af73efbdc5ad575 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:37:23 +0200 Subject: [PATCH 460/502] chore(deps): bump github.com/bmatcuk/doublestar/v4 from 4.8.0 to 4.8.1 (#701) Bumps [github.com/bmatcuk/doublestar/v4](https://github.com/bmatcuk/doublestar) from 4.8.0 to 4.8.1. - [Release notes](https://github.com/bmatcuk/doublestar/releases) - [Commits](https://github.com/bmatcuk/doublestar/compare/v4.8.0...v4.8.1) --- updated-dependencies: - dependency-name: github.com/bmatcuk/doublestar/v4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 47feab9..f1e81e8 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/zitadel/oidc/v3 go 1.21 require ( - github.com/bmatcuk/doublestar/v4 v4.8.0 + github.com/bmatcuk/doublestar/v4 v4.8.1 github.com/go-chi/chi/v5 v5.2.0 github.com/go-jose/go-jose/v4 v4.0.4 github.com/golang/mock v1.6.0 diff --git a/go.sum b/go.sum index 60a5125..a300634 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/bmatcuk/doublestar/v4 v4.8.0 h1:DSXtrypQddoug1459viM9X9D3dp1Z7993fw36I2kNcQ= -github.com/bmatcuk/doublestar/v4 v4.8.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= +github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 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= From 8c9a5360587d988691f7b27ed1c2fb9d5e49fc00 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:29:28 +0200 Subject: [PATCH 461/502] chore(deps): bump codecov/codecov-action from 5.1.2 to 5.3.1 (#703) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.1.2 to 5.3.1. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v5.1.2...v5.3.1) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f70bd8b..9969c58 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v5.1.2 + - uses: codecov/codecov-action@v5.3.1 with: file: ./profile.cov name: codecov-go From 4250aad1f7b2ea422153af88096bd366d290124f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:08:45 +0200 Subject: [PATCH 462/502] chore(deps): bump golang.org/x/oauth2 from 0.25.0 to 0.26.0 (#707) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.25.0 to 0.26.0. - [Commits](https://github.com/golang/oauth2/compare/v0.25.0...v0.26.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f1e81e8..f29b4bd 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/zitadel/logging v0.6.1 github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.29.0 - golang.org/x/oauth2 v0.25.0 + golang.org/x/oauth2 v0.26.0 golang.org/x/text v0.21.0 ) diff --git a/go.sum b/go.sum index a300634..8639d99 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= From 0d46df908ef4600e139ae5f6fae8b20c7166571b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 10:11:18 +0000 Subject: [PATCH 463/502] chore(deps): bump golang.org/x/text from 0.21.0 to 0.22.0 (#708) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.21.0 to 0.22.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.21.0...v0.22.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f29b4bd..e48719b 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.29.0 golang.org/x/oauth2 v0.26.0 - golang.org/x/text v0.21.0 + golang.org/x/text v0.22.0 ) require ( diff --git a/go.sum b/go.sum index 8639d99..9be3f8d 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,8 @@ golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= From c3c1bd3a404fed7d21a536eadc5e3e375b056fc2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:45:18 +0200 Subject: [PATCH 464/502] chore(deps): bump github.com/go-chi/chi/v5 from 5.2.0 to 5.2.1 (#706) Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.2.0 to 5.2.1. - [Release notes](https://github.com/go-chi/chi/releases) - [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md) - [Commits](https://github.com/go-chi/chi/compare/v5.2.0...v5.2.1) --- updated-dependencies: - dependency-name: github.com/go-chi/chi/v5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e48719b..a4a71b9 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/bmatcuk/doublestar/v4 v4.8.1 - github.com/go-chi/chi/v5 v5.2.0 + github.com/go-chi/chi/v5 v5.2.1 github.com/go-jose/go-jose/v4 v4.0.4 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 diff --git a/go.sum b/go.sum index 9be3f8d..41fd786 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTS 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/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= -github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= +github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= From 03e5ff83453c58810f1180ff16e969c676fe3857 Mon Sep 17 00:00:00 2001 From: mqf20 Date: Thu, 13 Feb 2025 19:23:44 +0800 Subject: [PATCH 465/502] docs(example): add auth time (#700) --- example/server/storage/storage.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index d8b7a5d..4c5680e 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -151,6 +151,9 @@ func (s *Storage) CheckUsernamePassword(username, password, id string) error { // in this example we'll simply check the username / password and set a boolean to true // therefore we will also just check this boolean if the request / login has been finished request.done = true + + request.authTime = time.Now() + return nil } return fmt.Errorf("username or password wrong") From 37dd41e49b603cabf32e9b82089e456ce0537626 Mon Sep 17 00:00:00 2001 From: mqf20 Date: Thu, 13 Feb 2025 19:26:00 +0800 Subject: [PATCH 466/502] docs(example): simplified deletion (#699) * simplified deletion * added docs --- example/server/storage/storage.go | 24 ++++++++++-------------- example/server/storage/token.go | 1 + 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index 4c5680e..6a66ca8 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -388,14 +388,9 @@ func (s *Storage) RevokeToken(ctx context.Context, tokenIDOrToken string, userID if refreshToken.ApplicationID != clientID { return oidc.ErrInvalidClient().WithDescription("token was not issued for this client") } - // if it is a refresh token, you will have to remove the access token as well delete(s.refreshTokens, refreshToken.ID) - for _, accessToken := range s.tokens { - if accessToken.RefreshTokenID == refreshToken.ID { - delete(s.tokens, accessToken.ID) - return nil - } - } + // if it is a refresh token, you will have to remove the access token as well + delete(s.tokens, refreshToken.AccessToken) return nil } @@ -597,12 +592,17 @@ func (s *Storage) createRefreshToken(accessToken *Token, amr []string, authTime Audience: accessToken.Audience, Expiration: time.Now().Add(5 * time.Hour), Scopes: accessToken.Scopes, + AccessToken: accessToken.ID, } s.refreshTokens[token.ID] = token return token.Token, nil } // renewRefreshToken checks the provided refresh_token and creates a new one based on the current +// +// [Refresh Token Rotation] is implemented. +// +// [Refresh Token Rotation]: https://www.rfc-editor.org/rfc/rfc6819#section-5.2.2.3 func (s *Storage) renewRefreshToken(currentRefreshToken string) (string, string, error) { s.lock.Lock() defer s.lock.Unlock() @@ -610,14 +610,10 @@ func (s *Storage) renewRefreshToken(currentRefreshToken string) (string, string, if !ok { return "", "", fmt.Errorf("invalid refresh token") } - // deletes the refresh token and all access tokens which were issued based on this refresh token + // deletes the refresh token delete(s.refreshTokens, currentRefreshToken) - for _, token := range s.tokens { - if token.RefreshTokenID == currentRefreshToken { - delete(s.tokens, token.ID) - break - } - } + // delete the access token which was issued based on this refresh token + delete(s.tokens, refreshToken.AccessToken) // creates a new refresh token based on the current one token := uuid.NewString() refreshToken.Token = token diff --git a/example/server/storage/token.go b/example/server/storage/token.go index ad907e3..beab38c 100644 --- a/example/server/storage/token.go +++ b/example/server/storage/token.go @@ -22,4 +22,5 @@ type RefreshToken struct { ApplicationID string Expiration time.Time Scopes []string + AccessToken string // Token.ID } From c03a8c59ca8e7fa49f903bb184793cc735834320 Mon Sep 17 00:00:00 2001 From: mqf20 Date: Thu, 13 Feb 2025 19:34:29 +0800 Subject: [PATCH 467/502] docs(example): check access token expiration (#702) --- example/server/storage/storage.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index 6a66ca8..b687a2c 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -486,6 +486,9 @@ func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserI // return err // } //} + if token.Expiration.Before(time.Now()) { + return fmt.Errorf("token is expired") + } return s.setUserinfo(ctx, userinfo, token.Subject, token.ApplicationID, token.Scopes) } From b1e5aca6298b02e5bd2ed7ae52b37cea0e0ee202 Mon Sep 17 00:00:00 2001 From: mqf20 Date: Thu, 13 Feb 2025 19:48:04 +0800 Subject: [PATCH 468/502] docs(example): check and extend refresh token expiration (#698) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * extend refresh token expiration * check refresh token expiration * check refresh token expiration (fixed logic) * formatting --------- Co-authored-by: Tim Möhlmann --- example/server/storage/storage.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index b687a2c..1bc2e94 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -615,12 +615,19 @@ func (s *Storage) renewRefreshToken(currentRefreshToken string) (string, string, } // deletes the refresh token delete(s.refreshTokens, currentRefreshToken) + // delete the access token which was issued based on this refresh token delete(s.tokens, refreshToken.AccessToken) + + if refreshToken.Expiration.Before(time.Now()) { + return "", "", fmt.Errorf("expired refresh token") + } + // creates a new refresh token based on the current one token := uuid.NewString() refreshToken.Token = token refreshToken.ID = token + refreshToken.Expiration = time.Now().Add(5 * time.Hour) s.refreshTokens[token] = refreshToken return token, refreshToken.ID, nil } From add254f60c9dcb4fcac9606f5adba99bd09d8113 Mon Sep 17 00:00:00 2001 From: mqf20 Date: Wed, 19 Feb 2025 20:44:34 +0800 Subject: [PATCH 469/502] docs(example): fixed creation of refresh token (#711) Signed-off-by: mqf20 --- example/server/storage/storage.go | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index 1bc2e94..fee34c5 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -298,15 +298,19 @@ func (s *Storage) CreateAccessAndRefreshTokens(ctx context.Context, request op.T // if we get here, the currentRefreshToken was not empty, so the call is a refresh token request // we therefore will have to check the currentRefreshToken and renew the refresh token - refreshToken, refreshTokenID, err := s.renewRefreshToken(currentRefreshToken) + + newRefreshToken = uuid.NewString() + + accessToken, err := s.accessToken(applicationID, newRefreshToken, request.GetSubject(), request.GetAudience(), request.GetScopes()) if err != nil { return "", "", time.Time{}, err } - accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes()) - if err != nil { + + if err := s.renewRefreshToken(currentRefreshToken, newRefreshToken, accessToken.ID); err != nil { return "", "", time.Time{}, err } - return accessToken.ID, refreshToken, accessToken.Expiration, nil + + return accessToken.ID, newRefreshToken, accessToken.Expiration, nil } func (s *Storage) exchangeRefreshToken(ctx context.Context, request op.TokenExchangeRequest) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) { @@ -606,12 +610,12 @@ func (s *Storage) createRefreshToken(accessToken *Token, amr []string, authTime // [Refresh Token Rotation] is implemented. // // [Refresh Token Rotation]: https://www.rfc-editor.org/rfc/rfc6819#section-5.2.2.3 -func (s *Storage) renewRefreshToken(currentRefreshToken string) (string, string, error) { +func (s *Storage) renewRefreshToken(currentRefreshToken, newRefreshToken, newAccessToken string) error { s.lock.Lock() defer s.lock.Unlock() refreshToken, ok := s.refreshTokens[currentRefreshToken] if !ok { - return "", "", fmt.Errorf("invalid refresh token") + return fmt.Errorf("invalid refresh token") } // deletes the refresh token delete(s.refreshTokens, currentRefreshToken) @@ -620,16 +624,16 @@ func (s *Storage) renewRefreshToken(currentRefreshToken string) (string, string, delete(s.tokens, refreshToken.AccessToken) if refreshToken.Expiration.Before(time.Now()) { - return "", "", fmt.Errorf("expired refresh token") + return fmt.Errorf("expired refresh token") } // creates a new refresh token based on the current one - token := uuid.NewString() - refreshToken.Token = token - refreshToken.ID = token + refreshToken.Token = newRefreshToken + refreshToken.ID = newRefreshToken refreshToken.Expiration = time.Now().Add(5 * time.Hour) - s.refreshTokens[token] = refreshToken - return token, refreshToken.ID, nil + refreshToken.AccessToken = newAccessToken + s.refreshTokens[newRefreshToken] = refreshToken + return nil } // accessToken will store an access_token in-memory based on the provided information From eb98343a65d0734071746d8d8ffd48db03207c28 Mon Sep 17 00:00:00 2001 From: Steve Ruckdashel Date: Fri, 21 Feb 2025 02:52:02 -0700 Subject: [PATCH 470/502] fix: migrate deprecated io/ioutil.ReadFile to os.ReadFile (#714) --- pkg/client/key.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/client/key.go b/pkg/client/key.go index 0c01dd2..7f38311 100644 --- a/pkg/client/key.go +++ b/pkg/client/key.go @@ -2,7 +2,7 @@ package client import ( "encoding/json" - "io/ioutil" + "os" ) const ( @@ -24,7 +24,7 @@ type KeyFile struct { } func ConfigFromKeyFile(path string) (*KeyFile, error) { - data, err := ioutil.ReadFile(path) + data, err := os.ReadFile(path) if err != nil { return nil, err } From 4ef9529012125e5c5e1e7778396a9267e3b8dfba Mon Sep 17 00:00:00 2001 From: minami yoshihiko Date: Mon, 24 Feb 2025 19:50:38 +0900 Subject: [PATCH 471/502] feat: support for session_state (#712) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add default signature algorithm * implements session_state in auth_request.go * add test * Update pkg/op/auth_request.go link to the standard Co-authored-by: Tim Möhlmann * add check_session_iframe --------- Co-authored-by: Tim Möhlmann Co-authored-by: Tim Möhlmann --- example/server/storage/oidc.go | 9 +++++++++ pkg/oidc/error.go | 9 ++++++++- pkg/op/auth_request.go | 26 ++++++++++++++++++++------ pkg/op/auth_request_test.go | 28 ++++++++++++++++++++++++++++ pkg/op/config.go | 1 + pkg/op/discovery.go | 1 + pkg/op/error.go | 12 ++++++++++++ pkg/op/mock/configuration.mock.go | 14 ++++++++++++++ pkg/op/op.go | 4 ++++ 9 files changed, 97 insertions(+), 7 deletions(-) diff --git a/example/server/storage/oidc.go b/example/server/storage/oidc.go index 22c0295..c04877f 100644 --- a/example/server/storage/oidc.go +++ b/example/server/storage/oidc.go @@ -164,6 +164,15 @@ func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthReques } } +type AuthRequestWithSessionState struct { + *AuthRequest + SessionState string +} + +func (a *AuthRequestWithSessionState) GetSessionState() string { + return a.SessionState +} + type OIDCCodeChallenge struct { Challenge string Method string diff --git a/pkg/oidc/error.go b/pkg/oidc/error.go index 1100f73..d93cf44 100644 --- a/pkg/oidc/error.go +++ b/pkg/oidc/error.go @@ -133,6 +133,7 @@ type Error struct { ErrorType errorType `json:"error" schema:"error"` Description string `json:"error_description,omitempty" schema:"error_description,omitempty"` State string `json:"state,omitempty" schema:"state,omitempty"` + SessionState string `json:"session_state,omitempty" schema:"session_state,omitempty"` redirectDisabled bool `schema:"-"` returnParent bool `schema:"-"` } @@ -142,11 +143,13 @@ func (e *Error) MarshalJSON() ([]byte, error) { Error errorType `json:"error"` ErrorDescription string `json:"error_description,omitempty"` State string `json:"state,omitempty"` + SessionState string `json:"session_state,omitempty"` Parent string `json:"parent,omitempty"` }{ Error: e.ErrorType, ErrorDescription: e.Description, State: e.State, + SessionState: e.SessionState, } if e.returnParent { m.Parent = e.Parent.Error() @@ -176,7 +179,8 @@ func (e *Error) Is(target error) bool { } return e.ErrorType == t.ErrorType && (e.Description == t.Description || t.Description == "") && - (e.State == t.State || t.State == "") + (e.State == t.State || t.State == "") && + (e.SessionState == t.SessionState || t.SessionState == "") } func (e *Error) WithParent(err error) *Error { @@ -242,6 +246,9 @@ func (e *Error) LogValue() slog.Value { if e.State != "" { attrs = append(attrs, slog.String("state", e.State)) } + if e.SessionState != "" { + attrs = append(attrs, slog.String("session_state", e.SessionState)) + } if e.redirectDisabled { attrs = append(attrs, slog.Bool("redirect_disabled", e.redirectDisabled)) } diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index d6db62b..82f1b58 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -38,6 +38,13 @@ type AuthRequest interface { Done() bool } +// AuthRequestSessionState should be implemented if [OpenID Connect Session Management](https://openid.net/specs/openid-connect-session-1_0.html) is supported +type AuthRequestSessionState interface { + // GetSessionState returns session_state. + // session_state is related to OpenID Connect Session Management. + GetSessionState() string +} + type Authorizer interface { Storage() Storage Decoder() httphelper.Decoder @@ -103,8 +110,8 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { } return ValidateAuthRequestClient(ctx, authReq, client, verifier) } - if validater, ok := authorizer.(AuthorizeValidator); ok { - validation = validater.ValidateAuthRequest + if validator, ok := authorizer.(AuthorizeValidator); ok { + validation = validator.ValidateAuthRequest } userID, err := validation(ctx, authReq, authorizer.Storage(), authorizer.IDTokenHintVerifier(ctx)) if err != nil { @@ -481,12 +488,19 @@ func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthReques AuthRequestError(w, r, authReq, err, authorizer) return } + var sessionState string + authRequestSessionState, ok := authReq.(AuthRequestSessionState) + if ok { + sessionState = authRequestSessionState.GetSessionState() + } codeResponse := struct { - Code string `schema:"code"` - State string `schema:"state,omitempty"` + Code string `schema:"code"` + State string `schema:"state,omitempty"` + SessionState string `schema:"session_state,omitempty"` }{ - Code: code, - State: authReq.GetState(), + Code: code, + State: authReq.GetState(), + SessionState: sessionState, } if authReq.GetResponseMode() == oidc.ResponseModeFormPost { diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index 765e602..4878f5e 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -1090,6 +1090,34 @@ func TestAuthResponseCode(t *testing.T) { wantBody: "", }, }, + { + name: "success with state and session_state", + args: args{ + authReq: &storage.AuthRequestWithSessionState{ + AuthRequest: &storage.AuthRequest{ + ID: "id1", + TransferState: "state1", + }, + SessionState: "session_state1", + }, + authorizer: func(t *testing.T) op.Authorizer { + ctrl := gomock.NewController(t) + storage := mock.NewMockStorage(ctrl) + storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1") + + authorizer := mock.NewMockAuthorizer(ctrl) + authorizer.EXPECT().Storage().Return(storage) + authorizer.EXPECT().Crypto().Return(&mockCrypto{}) + authorizer.EXPECT().Encoder().Return(schema.NewEncoder()) + return authorizer + }, + }, + res: res{ + wantCode: http.StatusFound, + wantLocationHeader: "/auth/callback/?code=id1&session_state=session_state1&state=state1", + wantBody: "", + }, + }, { name: "success without state", // reproduce issue #415 args: args{ diff --git a/pkg/op/config.go b/pkg/op/config.go index 2fcede0..b271765 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -30,6 +30,7 @@ type Configuration interface { EndSessionEndpoint() *Endpoint KeysEndpoint() *Endpoint DeviceAuthorizationEndpoint() *Endpoint + CheckSessionIframe() *Endpoint AuthMethodPostSupported() bool CodeMethodS256Supported() bool diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index e30a5a4..7aa7cf7 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -45,6 +45,7 @@ func CreateDiscoveryConfig(ctx context.Context, config Configuration, storage Di EndSessionEndpoint: config.EndSessionEndpoint().Absolute(issuer), JwksURI: config.KeysEndpoint().Absolute(issuer), DeviceAuthorizationEndpoint: config.DeviceAuthorizationEndpoint().Absolute(issuer), + CheckSessionIframe: config.CheckSessionIframe().Absolute(issuer), ScopesSupported: Scopes(config), ResponseTypesSupported: ResponseTypes(config), GrantTypesSupported: GrantTypes(config), diff --git a/pkg/op/error.go b/pkg/op/error.go index 44b1798..d57da83 100644 --- a/pkg/op/error.go +++ b/pkg/op/error.go @@ -46,6 +46,12 @@ func AuthRequestError(w http.ResponseWriter, r *http.Request, authReq ErrAuthReq return } e.State = authReq.GetState() + var sessionState string + authRequestSessionState, ok := authReq.(AuthRequestSessionState) + if ok { + sessionState = authRequestSessionState.GetSessionState() + } + e.SessionState = sessionState var responseMode oidc.ResponseMode if rm, ok := authReq.(interface{ GetResponseMode() oidc.ResponseMode }); ok { responseMode = rm.GetResponseMode() @@ -92,6 +98,12 @@ func TryErrorRedirect(ctx context.Context, authReq ErrAuthRequest, parent error, } e.State = authReq.GetState() + var sessionState string + authRequestSessionState, ok := authReq.(AuthRequestSessionState) + if ok { + sessionState = authRequestSessionState.GetSessionState() + } + e.SessionState = sessionState var responseMode oidc.ResponseMode if rm, ok := authReq.(interface{ GetResponseMode() oidc.ResponseMode }); ok { responseMode = rm.GetResponseMode() diff --git a/pkg/op/mock/configuration.mock.go b/pkg/op/mock/configuration.mock.go index 137c09d..0ef9d92 100644 --- a/pkg/op/mock/configuration.mock.go +++ b/pkg/op/mock/configuration.mock.go @@ -106,6 +106,20 @@ func (mr *MockConfigurationMockRecorder) BackChannelLogoutSupported() *gomock.Ca return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BackChannelLogoutSupported", reflect.TypeOf((*MockConfiguration)(nil).BackChannelLogoutSupported)) } +// CheckSessionIframe mocks base method. +func (m *MockConfiguration) CheckSessionIframe() *op.Endpoint { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckSessionIframe") + ret0, _ := ret[0].(*op.Endpoint) + return ret0 +} + +// CheckSessionIframe indicates an expected call of CheckSessionIframe. +func (mr *MockConfigurationMockRecorder) CheckSessionIframe() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckSessionIframe", reflect.TypeOf((*MockConfiguration)(nil).CheckSessionIframe)) +} + // CodeMethodS256Supported mocks base method. func (m *MockConfiguration) CodeMethodS256Supported() bool { m.ctrl.T.Helper() diff --git a/pkg/op/op.go b/pkg/op/op.go index 190c2c4..58ae838 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -339,6 +339,10 @@ func (o *Provider) DeviceAuthorizationEndpoint() *Endpoint { return o.endpoints.DeviceAuthorization } +func (o *Provider) CheckSessionIframe() *Endpoint { + return o.endpoints.CheckSessionIframe +} + func (o *Provider) KeysEndpoint() *Endpoint { return o.endpoints.JwksURI } From 6a80712fbe4b767c77b4483d46b0b2ce0c2b04e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 12:00:02 +0200 Subject: [PATCH 472/502] chore(deps): bump github.com/go-jose/go-jose/v4 from 4.0.4 to 4.0.5 (#716) Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.0.4 to 4.0.5. - [Release notes](https://github.com/go-jose/go-jose/releases) - [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md) - [Commits](https://github.com/go-jose/go-jose/compare/v4.0.4...v4.0.5) --- updated-dependencies: - dependency-name: github.com/go-jose/go-jose/v4 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index a4a71b9..70ace65 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 require ( github.com/bmatcuk/doublestar/v4 v4.8.1 github.com/go-chi/chi/v5 v5.2.1 - github.com/go-jose/go-jose/v4 v4.0.4 + github.com/go-jose/go-jose/v4 v4.0.5 github.com/golang/mock v1.6.0 github.com/google/go-github/v31 v31.0.0 github.com/google/uuid v1.6.0 @@ -31,8 +31,8 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/otel/metric v1.29.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect - golang.org/x/crypto v0.31.0 // indirect + golang.org/x/crypto v0.32.0 // indirect golang.org/x/net v0.33.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/sys v0.29.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 41fd786..03ecdfd 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ 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/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= -github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= -github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -62,8 +62,8 @@ go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt3 go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -83,8 +83,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From eb2f912c5e5a783e6fb682d5eeea3a13b1ad12c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Mar 2025 16:37:54 +0100 Subject: [PATCH 473/502] chore(deps): bump codecov/codecov-action from 5.3.1 to 5.4.0 (#722) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.3.1 to 5.4.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v5.3.1...v5.4.0) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9969c58..c4f6f68 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v5.3.1 + - uses: codecov/codecov-action@v5.4.0 with: file: ./profile.cov name: codecov-go From 7a767d8568772197503dca7f90b5a767f6f23572 Mon Sep 17 00:00:00 2001 From: BitMasher <61257372+BitMasher@users.noreply.github.com> Date: Wed, 12 Mar 2025 07:00:29 -0500 Subject: [PATCH 474/502] feat: add CanGetPrivateClaimsFromRequest interface (#717) --- pkg/op/storage.go | 6 ++++++ pkg/op/token.go | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 8488b28..a579810 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -144,6 +144,12 @@ type CanSetUserinfoFromRequest interface { SetUserinfoFromRequest(ctx context.Context, userinfo *oidc.UserInfo, request IDTokenRequest, scopes []string) error } +// CanGetPrivateClaimsFromRequest is an optional additional interface that may be implemented by +// implementors of Storage. It allows setting the jwt token claims based on the request. +type CanGetPrivateClaimsFromRequest interface { + GetPrivateClaimsFromRequest(ctx context.Context, request TokenRequest, restrictedScopes []string) (map[string]any, error) +} + // Storage is a required parameter for NewOpenIDProvider(). In addition to the // embedded interfaces below, if the passed Storage implements ClientCredentialsStorage // then the grant type "client_credentials" will be supported. In that case, the access diff --git a/pkg/op/token.go b/pkg/op/token.go index 04cd3cc..1df9cc2 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -147,7 +147,11 @@ func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, ex tokenExchangeRequest, ) } else { - privateClaims, err = storage.GetPrivateClaimsFromScopes(ctx, tokenRequest.GetSubject(), client.GetID(), removeUserinfoScopes(restrictedScopes)) + if fromRequest, ok := storage.(CanGetPrivateClaimsFromRequest); ok { + privateClaims, err = fromRequest.GetPrivateClaimsFromRequest(ctx, tokenRequest, removeUserinfoScopes(restrictedScopes)) + } else { + privateClaims, err = storage.GetPrivateClaimsFromScopes(ctx, tokenRequest.GetSubject(), client.GetID(), removeUserinfoScopes(restrictedScopes)) + } } if err != nil { From efd6fdad7aa2382879821fde4d016236bb5c243a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 14 Mar 2025 12:30:08 +0200 Subject: [PATCH 475/502] fix: ignore empty json strings for locale (#678) * Revert "fix: ignore all unmarshal errors from locale (#673)" This reverts commit fbf009fe75dac732dde39e0eb6fe324b337675e0. * fix: ignore empty json strings for locale --- pkg/oidc/types.go | 22 +++++++++++++----- pkg/oidc/types_test.go | 51 ++++++++++++++++++++++++++++++------------ 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index 7063426..9b307bc 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -3,6 +3,7 @@ package oidc import ( "database/sql/driver" "encoding/json" + "errors" "fmt" "reflect" "strings" @@ -77,16 +78,25 @@ func (l *Locale) MarshalJSON() ([]byte, error) { } // UnmarshalJSON implements json.Unmarshaler. -// All unmarshal errors for are ignored. -// When an error is encountered, the containing tag will be set +// When [language.ValueError] is encountered, the containing tag will be set // to an empty value (language "und") and no error will be returned. // This state can be checked with the `l.Tag().IsRoot()` method. func (l *Locale) UnmarshalJSON(data []byte) error { - err := json.Unmarshal(data, &l.tag) - if err != nil { - l.tag = language.Tag{} + if len(data) == 0 || string(data) == "\"\"" { + return nil } - return nil + err := json.Unmarshal(data, &l.tag) + if err == nil { + return nil + } + + // catch "well-formed but unknown" errors + var target language.ValueError + if errors.As(err, &target) { + l.tag = language.Tag{} + return nil + } + return err } type Locales []language.Tag diff --git a/pkg/oidc/types_test.go b/pkg/oidc/types_test.go index c7ce0ee..53a9779 100644 --- a/pkg/oidc/types_test.go +++ b/pkg/oidc/types_test.go @@ -217,6 +217,30 @@ func TestLocale_UnmarshalJSON(t *testing.T) { want dst wantErr bool }{ + { + name: "value not present", + input: `{}`, + wantErr: false, + want: dst{ + Locale: nil, + }, + }, + { + name: "null", + input: `{"locale": null}`, + wantErr: false, + want: dst{ + Locale: nil, + }, + }, + { + name: "empty, ignored", + input: `{"locale": ""}`, + wantErr: false, + want: dst{ + Locale: &Locale{}, + }, + }, { name: "afrikaans, ok", input: `{"locale": "af"}`, @@ -232,23 +256,22 @@ func TestLocale_UnmarshalJSON(t *testing.T) { }, }, { - name: "bad form, error", - input: `{"locale": "g!!!!!"}`, - want: dst{ - Locale: &Locale{}, - }, + name: "bad form, error", + input: `{"locale": "g!!!!!"}`, + wantErr: true, }, } - for _, tt := range tests { - var got dst - err := json.Unmarshal([]byte(tt.input), &got) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - assert.Equal(t, tt.want, got) + t.Run(tt.name, func(t *testing.T) { + var got dst + err := json.Unmarshal([]byte(tt.input), &got) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) } } From 2c64de821d850fc4b6bcd9396012f2bdcad4f954 Mon Sep 17 00:00:00 2001 From: Iraq <66622793+kkrime@users.noreply.github.com> Date: Fri, 14 Mar 2025 15:12:26 +0000 Subject: [PATCH 476/502] chore: updating go to 1.24 (#726) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: updating go to 1.24 * fixup! chore: updating go to 1.24 * fixup! fixup! chore: updating go to 1.24 * fix device test (drop read error) * drop older go versions * drop unrelated formatter changes --------- Co-authored-by: Iraq Jaber Co-authored-by: Tim Möhlmann --- .github/workflows/release.yml | 2 +- README.md | 5 ++--- go.mod | 2 +- pkg/op/device.go | 14 +++++++------- pkg/op/device_test.go | 20 +++++--------------- 5 files changed, 16 insertions(+), 27 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c4f6f68..146f9f2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - go: ['1.21', '1.22', '1.23'] + go: ['1.23', '1.24'] name: Go ${{ matrix.go }} test steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index 04d551f..bc346f5 100644 --- a/README.md +++ b/README.md @@ -156,10 +156,9 @@ Versions that also build are marked with :warning:. | Version | Supported | | ------- | ------------------ | -| <1.21 | :x: | -| 1.21 | :warning: | -| 1.22 | :white_check_mark: | +| <1.23 | :x: | | 1.23 | :white_check_mark: | +| 1.24 | :white_check_mark: | ## Why another library diff --git a/go.mod b/go.mod index 70ace65..d92ef02 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/zitadel/oidc/v3 -go 1.21 +go 1.23.7 require ( github.com/bmatcuk/doublestar/v4 v4.8.1 diff --git a/pkg/op/device.go b/pkg/op/device.go index 8a0e174..b7290cd 100644 --- a/pkg/op/device.go +++ b/pkg/op/device.go @@ -91,10 +91,7 @@ func createDeviceAuthorization(ctx context.Context, req *oidc.DeviceAuthorizatio } config := o.DeviceAuthorization() - deviceCode, err := NewDeviceCode(RecommendedDeviceCodeBytes) - if err != nil { - return nil, NewStatusError(err, http.StatusInternalServerError) - } + deviceCode, _ := NewDeviceCode(RecommendedDeviceCodeBytes) userCode, err := NewUserCode([]rune(config.UserCode.CharSet), config.UserCode.CharAmount, config.UserCode.DashInterval) if err != nil { return nil, NewStatusError(err, http.StatusInternalServerError) @@ -163,11 +160,14 @@ func ParseDeviceCodeRequest(r *http.Request, o OpenIDProvider) (*oidc.DeviceAuth // results in a 22 character base64 encoded string. const RecommendedDeviceCodeBytes = 16 +// NewDeviceCode generates a new cryptographically secure device code as a base64 encoded string. +// The length of the string is nBytes * 4 / 3. +// An error is never returned. +// +// TODO(v4): change return type to string alone. func NewDeviceCode(nBytes int) (string, error) { bytes := make([]byte, nBytes) - if _, err := rand.Read(bytes); err != nil { - return "", fmt.Errorf("%w getting entropy for device code", err) - } + rand.Read(bytes) return base64.RawURLEncoding.EncodeToString(bytes), nil } diff --git a/pkg/op/device_test.go b/pkg/op/device_test.go index 570b943..5fd9c9b 100644 --- a/pkg/op/device_test.go +++ b/pkg/op/device_test.go @@ -145,21 +145,11 @@ func runWithRandReader(r io.Reader, f func()) { } func TestNewDeviceCode(t *testing.T) { - t.Run("reader error", func(t *testing.T) { - runWithRandReader(errReader{}, func() { - _, err := op.NewDeviceCode(16) - require.Error(t, err) - }) - }) - - t.Run("different lengths, rand reader", func(t *testing.T) { - for i := 1; i <= 32; i++ { - got, err := op.NewDeviceCode(i) - require.NoError(t, err) - assert.Len(t, got, base64.RawURLEncoding.EncodedLen(i)) - } - }) - + for i := 1; i <= 32; i++ { + got, err := op.NewDeviceCode(i) + require.NoError(t, err) + assert.Len(t, got, base64.RawURLEncoding.EncodedLen(i)) + } } func TestNewUserCode(t *testing.T) { From c401ad6cb8b98caa70ab4b6c08f9d317b528d53c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 07:46:07 +0100 Subject: [PATCH 477/502] chore(deps): bump golang.org/x/oauth2 from 0.26.0 to 0.28.0 (#724) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.26.0 to 0.28.0. - [Commits](https://github.com/golang/oauth2/compare/v0.26.0...v0.28.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d92ef02..01fd47a 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/zitadel/logging v0.6.1 github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.29.0 - golang.org/x/oauth2 v0.26.0 + golang.org/x/oauth2 v0.28.0 golang.org/x/text v0.22.0 ) diff --git a/go.sum b/go.sum index 03ecdfd..1d32f03 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= -golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= From f3ee6470052ad846de95ce30af80820fa86cc98a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 12:02:56 +0200 Subject: [PATCH 478/502] chore(deps): bump golang.org/x/net from 0.33.0 to 0.36.0 (#727) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.33.0 to 0.36.0. - [Commits](https://github.com/golang/net/compare/v0.33.0...v0.36.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 7 ++++--- go.sum | 12 ++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 01fd47a..3e93574 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module github.com/zitadel/oidc/v3 go 1.23.7 +toolchain go1.24.1 require ( github.com/bmatcuk/doublestar/v4 v4.8.1 @@ -31,8 +32,8 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/otel/metric v1.29.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect - golang.org/x/crypto v0.32.0 // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/crypto v0.35.0 // indirect + golang.org/x/net v0.36.0 // indirect + golang.org/x/sys v0.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1d32f03..013abc0 100644 --- a/go.sum +++ b/go.sum @@ -62,16 +62,16 @@ go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt3 go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= +golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= @@ -83,8 +83,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From aeda5d7178ac859757f8ad925df67da819975126 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 10:05:10 +0000 Subject: [PATCH 479/502] chore(deps): bump golang.org/x/text from 0.22.0 to 0.23.0 (#723) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.22.0 to 0.23.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.22.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3e93574..ded75c1 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.29.0 golang.org/x/oauth2 v0.28.0 - golang.org/x/text v0.22.0 + golang.org/x/text v0.23.0 ) require ( diff --git a/go.sum b/go.sum index 013abc0..f2dc9de 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,8 @@ golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= From 30acdaf63a5305fcf1efe29c2f605b7566478ba5 Mon Sep 17 00:00:00 2001 From: Iraq Jaber Date: Sun, 23 Mar 2025 16:27:57 +0000 Subject: [PATCH 480/502] chore: run 'go mod tidy' --- go.mod | 1 + 1 file changed, 1 insertion(+) diff --git a/go.mod b/go.mod index ded75c1..3c2a555 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module github.com/zitadel/oidc/v3 go 1.23.7 + toolchain go1.24.1 require ( From c91db9e47b147ce8c5d8ba33939782b6177f78bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 12:11:07 +0200 Subject: [PATCH 481/502] chore(deps): bump github.com/zitadel/logging from 0.6.1 to 0.6.2 (#730) Bumps [github.com/zitadel/logging](https://github.com/zitadel/logging) from 0.6.1 to 0.6.2. - [Release notes](https://github.com/zitadel/logging/releases) - [Changelog](https://github.com/zitadel/logging/blob/main/.releaserc.js) - [Commits](https://github.com/zitadel/logging/compare/v0.6.1...v0.6.2) --- updated-dependencies: - dependency-name: github.com/zitadel/logging dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 3c2a555..2dc816b 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/rs/cors v1.11.1 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 - github.com/zitadel/logging v0.6.1 + github.com/zitadel/logging v0.6.2 github.com/zitadel/schema v1.3.0 go.opentelemetry.io/otel v1.29.0 golang.org/x/oauth2 v0.28.0 diff --git a/go.sum b/go.sum index f2dc9de..6645565 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/zitadel/logging v0.6.1 h1:Vyzk1rl9Kq9RCevcpX6ujUaTYFX43aa4LkvV1TvUk+Y= -github.com/zitadel/logging v0.6.1/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= +github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU= +github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= @@ -101,8 +101,8 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 7096406e71f682c492da1a2d4b6a4a0b88ffd34a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 12:19:20 +0200 Subject: [PATCH 482/502] chore(deps): bump github.com/zitadel/schema from 1.3.0 to 1.3.1 (#731) Bumps [github.com/zitadel/schema](https://github.com/zitadel/schema) from 1.3.0 to 1.3.1. - [Release notes](https://github.com/zitadel/schema/releases) - [Changelog](https://github.com/zitadel/schema/blob/main/.releaserc.js) - [Commits](https://github.com/zitadel/schema/compare/v1.3.0...v1.3.1) --- updated-dependencies: - dependency-name: github.com/zitadel/schema dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2dc816b..d0d892a 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 github.com/zitadel/logging v0.6.2 - github.com/zitadel/schema v1.3.0 + github.com/zitadel/schema v1.3.1 go.opentelemetry.io/otel v1.29.0 golang.org/x/oauth2 v0.28.0 golang.org/x/text v0.23.0 diff --git a/go.sum b/go.sum index 6645565..e174d34 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU= github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4= -github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= -github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= +github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU= +github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU= go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= From c51628ea27035796152a32631439625b55f0a7ea Mon Sep 17 00:00:00 2001 From: Ayato Date: Tue, 25 Mar 2025 01:00:04 +0900 Subject: [PATCH 483/502] feat(op): always verify code challenge when available (#721) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finally the RFC Best Current Practice for OAuth 2.0 Security has been approved. According to the RFC: > Authorization servers MUST support PKCE [RFC7636]. > > If a client sends a valid PKCE code_challenge parameter in the authorization request, the authorization server MUST enforce the correct usage of code_verifier at the token endpoint. Isn’t it time we strengthen PKCE support a bit more? This PR updates the logic so that PKCE is always verified, even when the Auth Method is not "none". --- example/client/app/app.go | 12 +++++++++ example/server/exampleop/templates/login.html | 4 +-- example/server/storage/oidc.go | 15 +++++++---- pkg/op/op_test.go | 1 + pkg/op/server_http_routes_test.go | 2 +- pkg/op/token_code.go | 26 ++++++++++++++----- 6 files changed, 45 insertions(+), 15 deletions(-) diff --git a/example/client/app/app.go b/example/client/app/app.go index 0b9b19d..5740591 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -7,6 +7,7 @@ import ( "log/slog" "net/http" "os" + "strconv" "strings" "sync/atomic" "time" @@ -34,6 +35,14 @@ func main() { scopes := strings.Split(os.Getenv("SCOPES"), " ") responseMode := os.Getenv("RESPONSE_MODE") + var pkce bool + if pkceEnv, ok := os.LookupEnv("PKCE"); ok { + var err error + pkce, err = strconv.ParseBool(pkceEnv) + if err != nil { + logrus.Fatalf("error parsing PKCE %s", err.Error()) + } + } redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath) cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure()) @@ -64,6 +73,9 @@ func main() { if keyPath != "" { options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath))) } + if pkce { + options = append(options, rp.WithPKCE(cookieHandler)) + } // One can add a logger to the context, // pre-defining log attributes as required. diff --git a/example/server/exampleop/templates/login.html b/example/server/exampleop/templates/login.html index b048211..d7f8f9a 100644 --- a/example/server/exampleop/templates/login.html +++ b/example/server/exampleop/templates/login.html @@ -25,5 +25,5 @@ -` -{{- end }} \ No newline at end of file + +{{- end }} diff --git a/example/server/storage/oidc.go b/example/server/storage/oidc.go index c04877f..3d5d86b 100644 --- a/example/server/storage/oidc.go +++ b/example/server/storage/oidc.go @@ -18,7 +18,7 @@ const ( // CustomClaim is an example for how to return custom claims with this library CustomClaim = "custom_claim" - // CustomScopeImpersonatePrefix is an example scope prefix for passing user id to impersonate using token exchage + // CustomScopeImpersonatePrefix is an example scope prefix for passing user id to impersonate using token exchange CustomScopeImpersonatePrefix = "custom_scope:impersonate:" ) @@ -143,6 +143,14 @@ func MaxAgeToInternal(maxAge *uint) *time.Duration { } func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthRequest { + var codeChallenge *OIDCCodeChallenge + if authReq.CodeChallenge != "" { + codeChallenge = &OIDCCodeChallenge{ + Challenge: authReq.CodeChallenge, + Method: string(authReq.CodeChallengeMethod), + } + } + return &AuthRequest{ CreationDate: time.Now(), ApplicationID: authReq.ClientID, @@ -157,10 +165,7 @@ func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthReques ResponseType: authReq.ResponseType, ResponseMode: authReq.ResponseMode, Nonce: authReq.Nonce, - CodeChallenge: &OIDCCodeChallenge{ - Challenge: authReq.CodeChallenge, - Method: string(authReq.CodeChallengeMethod), - }, + CodeChallenge: codeChallenge, } } diff --git a/pkg/op/op_test.go b/pkg/op/op_test.go index 9a4a624..c1520e2 100644 --- a/pkg/op/op_test.go +++ b/pkg/op/op_test.go @@ -102,6 +102,7 @@ func TestRoutes(t *testing.T) { authReq, err := storage.CreateAuthRequest(ctx, oidcAuthReq, "id1") require.NoError(t, err) storage.AuthRequestDone(authReq.GetID()) + storage.SaveAuthCode(ctx, authReq.GetID(), "123") accessToken, refreshToken, _, err := op.CreateAccessToken(ctx, authReq, op.AccessTokenTypeBearer, testProvider, client, "") require.NoError(t, err) diff --git a/pkg/op/server_http_routes_test.go b/pkg/op/server_http_routes_test.go index 1bfb32b..e0e4a97 100644 --- a/pkg/op/server_http_routes_test.go +++ b/pkg/op/server_http_routes_test.go @@ -130,7 +130,7 @@ func TestServerRoutes(t *testing.T) { "client_id": client.GetID(), "client_secret": "secret", "redirect_uri": "https://example.com", - "code": "123", + "code": "abc", }, wantCode: http.StatusBadRequest, json: `{"error":"invalid_grant", "error_description":"invalid code"}`, diff --git a/pkg/op/token_code.go b/pkg/op/token_code.go index 3612240..019aa63 100644 --- a/pkg/op/token_code.go +++ b/pkg/op/token_code.go @@ -74,6 +74,20 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, ctx, span := tracer.Start(ctx, "AuthorizeCodeClient") defer span.End() + request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code) + if err != nil { + return nil, nil, err + } + + codeChallenge := request.GetCodeChallenge() + if codeChallenge != nil { + err = AuthorizeCodeChallenge(tokenReq.CodeVerifier, codeChallenge) + + if err != nil { + return nil, nil, err + } + } + if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion { jwtExchanger, ok := exchanger.(JWTAuthorizationGrantExchanger) if !ok || !exchanger.AuthMethodPrivateKeyJWTSupported() { @@ -83,9 +97,9 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, if err != nil { return nil, nil, err } - request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code) return request, client, err } + client, err = exchanger.Storage().GetClientByClientID(ctx, tokenReq.ClientID) if err != nil { return nil, nil, oidc.ErrInvalidClient().WithParent(err) @@ -94,12 +108,10 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, return nil, nil, oidc.ErrInvalidClient().WithDescription("private_key_jwt not allowed for this client") } if client.AuthMethod() == oidc.AuthMethodNone { - request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code) - if err != nil { - return nil, nil, err + if codeChallenge == nil { + return nil, nil, oidc.ErrInvalidRequest().WithDescription("PKCE required") } - err = AuthorizeCodeChallenge(tokenReq.CodeVerifier, request.GetCodeChallenge()) - return request, client, err + return request, client, nil } if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() { return nil, nil, oidc.ErrInvalidClient().WithDescription("auth_method post not supported") @@ -108,7 +120,7 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, if err != nil { return nil, nil, err } - request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code) + return request, client, err } From 92972fd30f02756cdf361c024342fbc8f2cb660c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:03:06 +0300 Subject: [PATCH 484/502] chore(deps): bump golang.org/x/oauth2 from 0.28.0 to 0.29.0 (#734) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.28.0 to 0.29.0. - [Commits](https://github.com/golang/oauth2/compare/v0.28.0...v0.29.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-version: 0.29.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d0d892a..ecfc3e8 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/zitadel/logging v0.6.2 github.com/zitadel/schema v1.3.1 go.opentelemetry.io/otel v1.29.0 - golang.org/x/oauth2 v0.28.0 + golang.org/x/oauth2 v0.29.0 golang.org/x/text v0.23.0 ) diff --git a/go.sum b/go.sum index e174d34..35d356f 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= -golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= From 7cc5fb656818b9da48d34252c186b3d715cf2af0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 12:05:26 +0000 Subject: [PATCH 485/502] chore(deps): bump golang.org/x/text from 0.23.0 to 0.24.0 (#733) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.23.0 to 0.24.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.23.0...v0.24.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-version: 0.24.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ecfc3e8..3ba70d1 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/zitadel/schema v1.3.1 go.opentelemetry.io/otel v1.29.0 golang.org/x/oauth2 v0.29.0 - golang.org/x/text v0.23.0 + golang.org/x/text v0.24.0 ) require ( diff --git a/go.sum b/go.sum index 35d356f..0052890 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,8 @@ golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= From cb3ec3ac5f9bcfb07ffb51c4230291408a3501de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 11:05:39 +0200 Subject: [PATCH 486/502] chore(deps): bump golang.org/x/net from 0.36.0 to 0.38.0 (#739) * chore(deps): bump golang.org/x/net from 0.36.0 to 0.38.0 Bumps [golang.org/x/net](https://github.com/golang/net) from 0.36.0 to 0.38.0. - [Commits](https://github.com/golang/net/compare/v0.36.0...v0.38.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.38.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] * update runner to ubuntu 24.04 --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Livio Spring --- .github/workflows/release.yml | 4 ++-- go.mod | 6 +++--- go.sum | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 146f9f2..36ad346 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ on: jobs: test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: @@ -32,7 +32,7 @@ jobs: file: ./profile.cov name: codecov-go release: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 needs: [test] if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next' }} env: diff --git a/go.mod b/go.mod index 3ba70d1..f5ad96b 100644 --- a/go.mod +++ b/go.mod @@ -33,8 +33,8 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/otel/metric v1.29.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect - golang.org/x/crypto v0.35.0 // indirect - golang.org/x/net v0.36.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sys v0.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0052890..e0ac4f5 100644 --- a/go.sum +++ b/go.sum @@ -62,16 +62,16 @@ go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt3 go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= -golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= -golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= @@ -83,8 +83,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From b917cdc2e3cc021815e28e30bde8c3ea688aa339 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 11:13:43 +0200 Subject: [PATCH 487/502] chore(deps): bump codecov/codecov-action from 5.4.0 to 5.4.2 (#737) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.4.0 to 5.4.2. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v5.4.0...v5.4.2) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: 5.4.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 36ad346..20cb6df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v5.4.0 + - uses: codecov/codecov-action@v5.4.2 with: file: ./profile.cov name: codecov-go From 5913c5a07482829532d831b168a82255cba0e8cc Mon Sep 17 00:00:00 2001 From: Masahito Osako <43847020+m11o@users.noreply.github.com> Date: Tue, 29 Apr 2025 23:17:28 +0900 Subject: [PATCH 488/502] feat: enhance authentication response handling (#728) - Introduced CodeResponseType struct to encapsulate response data. - Added handleFormPostResponse and handleRedirectResponse functions to manage different response modes. - Created BuildAuthResponseCodeResponsePayload and BuildAuthResponseCallbackURL functions for better modularity in response generation. --- pkg/op/auth_request.go | 82 ++++++--- pkg/op/auth_request_test.go | 355 ++++++++++++++++++++++++++++++++++++ 2 files changed, 410 insertions(+), 27 deletions(-) diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 82f1b58..2c013aa 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -62,6 +62,12 @@ type AuthorizeValidator interface { ValidateAuthRequest(context.Context, *oidc.AuthRequest, Storage, *IDTokenHintVerifier) (string, error) } +type CodeResponseType struct { + Code string `schema:"code"` + State string `schema:"state,omitempty"` + SessionState string `schema:"session_state,omitempty"` +} + func authorizeHandler(authorizer Authorizer) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { Authorize(w, r, authorizer) @@ -477,48 +483,70 @@ func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWri AuthResponseToken(w, r, authReq, authorizer, client) } -// AuthResponseCode creates the successful code authentication response +// AuthResponseCode handles the creation of a successful authentication response using an authorization code func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer) { ctx, span := tracer.Start(r.Context(), "AuthResponseCode") - r = r.WithContext(ctx) defer span.End() + r = r.WithContext(ctx) + + var err error + if authReq.GetResponseMode() == oidc.ResponseModeFormPost { + err = handleFormPostResponse(w, r, authReq, authorizer) + } else { + err = handleRedirectResponse(w, r, authReq, authorizer) + } - code, err := CreateAuthRequestCode(r.Context(), authReq, authorizer.Storage(), authorizer.Crypto()) if err != nil { AuthRequestError(w, r, authReq, err, authorizer) - return } - var sessionState string - authRequestSessionState, ok := authReq.(AuthRequestSessionState) - if ok { +} + +// handleFormPostResponse processes the authentication response using form post method +func handleFormPostResponse(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer) error { + codeResponse, err := BuildAuthResponseCodeResponsePayload(r.Context(), authReq, authorizer) + if err != nil { + return err + } + return AuthResponseFormPost(w, authReq.GetRedirectURI(), codeResponse, authorizer.Encoder()) +} + +// handleRedirectResponse processes the authentication response using the redirect method +func handleRedirectResponse(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer) error { + callbackURL, err := BuildAuthResponseCallbackURL(r.Context(), authReq, authorizer) + if err != nil { + return err + } + http.Redirect(w, r, callbackURL, http.StatusFound) + return nil +} + +// BuildAuthResponseCodeResponsePayload generates the authorization code response payload for the authentication request +func BuildAuthResponseCodeResponsePayload(ctx context.Context, authReq AuthRequest, authorizer Authorizer) (*CodeResponseType, error) { + code, err := CreateAuthRequestCode(ctx, authReq, authorizer.Storage(), authorizer.Crypto()) + if err != nil { + return nil, err + } + + sessionState := "" + if authRequestSessionState, ok := authReq.(AuthRequestSessionState); ok { sessionState = authRequestSessionState.GetSessionState() } - codeResponse := struct { - Code string `schema:"code"` - State string `schema:"state,omitempty"` - SessionState string `schema:"session_state,omitempty"` - }{ + + return &CodeResponseType{ Code: code, State: authReq.GetState(), SessionState: sessionState, - } + }, nil +} - if authReq.GetResponseMode() == oidc.ResponseModeFormPost { - err := AuthResponseFormPost(w, authReq.GetRedirectURI(), &codeResponse, authorizer.Encoder()) - if err != nil { - AuthRequestError(w, r, authReq, err, authorizer) - return - } - - return - } - - callback, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), &codeResponse, authorizer.Encoder()) +// BuildAuthResponseCallbackURL generates the callback URL for a successful authorization code response +func BuildAuthResponseCallbackURL(ctx context.Context, authReq AuthRequest, authorizer Authorizer) (string, error) { + codeResponse, err := BuildAuthResponseCodeResponsePayload(ctx, authReq, authorizer) if err != nil { - AuthRequestError(w, r, authReq, err, authorizer) - return + return "", err } - http.Redirect(w, r, callback, http.StatusFound) + + return AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), codeResponse, authorizer.Encoder()) } // AuthResponseToken creates the successful token(s) authentication response diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index 4878f5e..f0c4ef1 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -1225,6 +1225,133 @@ func Test_parseAuthorizeCallbackRequest(t *testing.T) { } } +func TestBuildAuthResponseCodeResponsePayload(t *testing.T) { + type args struct { + authReq op.AuthRequest + authorizer func(*testing.T) op.Authorizer + } + type res struct { + wantCode string + wantState string + wantSessionState string + wantErr bool + } + tests := []struct { + name string + args args + res res + }{ + { + name: "create code error", + args: args{ + authReq: &storage.AuthRequest{ + ID: "id1", + }, + authorizer: func(t *testing.T) op.Authorizer { + ctrl := gomock.NewController(t) + storage := mock.NewMockStorage(ctrl) + + authorizer := mock.NewMockAuthorizer(ctrl) + authorizer.EXPECT().Storage().Return(storage) + authorizer.EXPECT().Crypto().Return(&mockCrypto{ + returnErr: io.ErrClosedPipe, + }) + return authorizer + }, + }, + res: res{ + wantErr: true, + }, + }, + { + name: "success with state", + args: args{ + authReq: &storage.AuthRequest{ + ID: "id1", + TransferState: "state1", + }, + authorizer: func(t *testing.T) op.Authorizer { + ctrl := gomock.NewController(t) + storage := mock.NewMockStorage(ctrl) + storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1") + + authorizer := mock.NewMockAuthorizer(ctrl) + authorizer.EXPECT().Storage().Return(storage) + authorizer.EXPECT().Crypto().Return(&mockCrypto{}) + return authorizer + }, + }, + res: res{ + wantCode: "id1", + wantState: "state1", + }, + }, + { + name: "success without state", + args: args{ + authReq: &storage.AuthRequest{ + ID: "id1", + TransferState: "", + }, + authorizer: func(t *testing.T) op.Authorizer { + ctrl := gomock.NewController(t) + storage := mock.NewMockStorage(ctrl) + storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1") + + authorizer := mock.NewMockAuthorizer(ctrl) + authorizer.EXPECT().Storage().Return(storage) + authorizer.EXPECT().Crypto().Return(&mockCrypto{}) + return authorizer + }, + }, + res: res{ + wantCode: "id1", + wantState: "", + }, + }, + { + name: "success with session_state", + args: args{ + authReq: &storage.AuthRequestWithSessionState{ + AuthRequest: &storage.AuthRequest{ + ID: "id1", + TransferState: "state1", + }, + SessionState: "session_state1", + }, + authorizer: func(t *testing.T) op.Authorizer { + ctrl := gomock.NewController(t) + storage := mock.NewMockStorage(ctrl) + storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1") + + authorizer := mock.NewMockAuthorizer(ctrl) + authorizer.EXPECT().Storage().Return(storage) + authorizer.EXPECT().Crypto().Return(&mockCrypto{}) + return authorizer + }, + }, + res: res{ + wantCode: "id1", + wantState: "state1", + wantSessionState: "session_state1", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := op.BuildAuthResponseCodeResponsePayload(context.Background(), tt.args.authReq, tt.args.authorizer(t)) + if tt.res.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.res.wantCode, got.Code) + assert.Equal(t, tt.res.wantState, got.State) + assert.Equal(t, tt.res.wantSessionState, got.SessionState) + }) + } +} + func TestValidateAuthReqIDTokenHint(t *testing.T) { token, _ := tu.ValidIDToken() tests := []struct { @@ -1255,3 +1382,231 @@ func TestValidateAuthReqIDTokenHint(t *testing.T) { }) } } + +func TestBuildAuthResponseCallbackURL(t *testing.T) { + type args struct { + authReq op.AuthRequest + authorizer func(*testing.T) op.Authorizer + } + type res struct { + wantURL string + wantErr bool + } + tests := []struct { + name string + args args + res res + }{ + { + name: "error when generating code response", + args: args{ + authReq: &storage.AuthRequest{ + ID: "id1", + }, + authorizer: func(t *testing.T) op.Authorizer { + ctrl := gomock.NewController(t) + storage := mock.NewMockStorage(ctrl) + + authorizer := mock.NewMockAuthorizer(ctrl) + authorizer.EXPECT().Storage().Return(storage) + authorizer.EXPECT().Crypto().Return(&mockCrypto{ + returnErr: io.ErrClosedPipe, + }) + return authorizer + }, + }, + res: res{ + wantErr: true, + }, + }, + { + name: "error when generating callback URL", + args: args{ + authReq: &storage.AuthRequest{ + ID: "id1", + CallbackURI: "://invalid-url", + }, + authorizer: func(t *testing.T) op.Authorizer { + ctrl := gomock.NewController(t) + storage := mock.NewMockStorage(ctrl) + storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1") + + authorizer := mock.NewMockAuthorizer(ctrl) + authorizer.EXPECT().Storage().Return(storage) + authorizer.EXPECT().Crypto().Return(&mockCrypto{}) + authorizer.EXPECT().Encoder().Return(schema.NewEncoder()) + return authorizer + }, + }, + res: res{ + wantErr: true, + }, + }, + { + name: "success with state", + args: args{ + authReq: &storage.AuthRequest{ + ID: "id1", + CallbackURI: "https://example.com/callback", + TransferState: "state1", + }, + authorizer: func(t *testing.T) op.Authorizer { + ctrl := gomock.NewController(t) + storage := mock.NewMockStorage(ctrl) + storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1") + + authorizer := mock.NewMockAuthorizer(ctrl) + authorizer.EXPECT().Storage().Return(storage) + authorizer.EXPECT().Crypto().Return(&mockCrypto{}) + authorizer.EXPECT().Encoder().Return(schema.NewEncoder()) + return authorizer + }, + }, + res: res{ + wantURL: "https://example.com/callback?code=id1&state=state1", + wantErr: false, + }, + }, + { + name: "success without state", + args: args{ + authReq: &storage.AuthRequest{ + ID: "id1", + CallbackURI: "https://example.com/callback", + }, + authorizer: func(t *testing.T) op.Authorizer { + ctrl := gomock.NewController(t) + storage := mock.NewMockStorage(ctrl) + storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1") + + authorizer := mock.NewMockAuthorizer(ctrl) + authorizer.EXPECT().Storage().Return(storage) + authorizer.EXPECT().Crypto().Return(&mockCrypto{}) + authorizer.EXPECT().Encoder().Return(schema.NewEncoder()) + return authorizer + }, + }, + res: res{ + wantURL: "https://example.com/callback?code=id1", + wantErr: false, + }, + }, + { + name: "success with session_state", + args: args{ + authReq: &storage.AuthRequestWithSessionState{ + AuthRequest: &storage.AuthRequest{ + ID: "id1", + CallbackURI: "https://example.com/callback", + TransferState: "state1", + }, + SessionState: "session_state1", + }, + authorizer: func(t *testing.T) op.Authorizer { + ctrl := gomock.NewController(t) + storage := mock.NewMockStorage(ctrl) + storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1") + + authorizer := mock.NewMockAuthorizer(ctrl) + authorizer.EXPECT().Storage().Return(storage) + authorizer.EXPECT().Crypto().Return(&mockCrypto{}) + authorizer.EXPECT().Encoder().Return(schema.NewEncoder()) + return authorizer + }, + }, + res: res{ + wantURL: "https://example.com/callback?code=id1&session_state=session_state1&state=state1", + wantErr: false, + }, + }, + { + name: "success with existing query parameters", + args: args{ + authReq: &storage.AuthRequest{ + ID: "id1", + CallbackURI: "https://example.com/callback?param=value", + TransferState: "state1", + }, + authorizer: func(t *testing.T) op.Authorizer { + ctrl := gomock.NewController(t) + storage := mock.NewMockStorage(ctrl) + storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1") + + authorizer := mock.NewMockAuthorizer(ctrl) + authorizer.EXPECT().Storage().Return(storage) + authorizer.EXPECT().Crypto().Return(&mockCrypto{}) + authorizer.EXPECT().Encoder().Return(schema.NewEncoder()) + return authorizer + }, + }, + res: res{ + wantURL: "https://example.com/callback?param=value&code=id1&state=state1", + wantErr: false, + }, + }, + { + name: "success with fragment response mode", + args: args{ + authReq: &storage.AuthRequest{ + ID: "id1", + CallbackURI: "https://example.com/callback", + TransferState: "state1", + ResponseMode: "fragment", + }, + authorizer: func(t *testing.T) op.Authorizer { + ctrl := gomock.NewController(t) + storage := mock.NewMockStorage(ctrl) + storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1") + + authorizer := mock.NewMockAuthorizer(ctrl) + authorizer.EXPECT().Storage().Return(storage) + authorizer.EXPECT().Crypto().Return(&mockCrypto{}) + authorizer.EXPECT().Encoder().Return(schema.NewEncoder()) + return authorizer + }, + }, + res: res{ + wantURL: "https://example.com/callback#code=id1&state=state1", + wantErr: false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := op.BuildAuthResponseCallbackURL(context.Background(), tt.args.authReq, tt.args.authorizer(t)) + if tt.res.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + + if tt.res.wantURL != "" { + // Parse the URLs to compare components instead of direct string comparison + expectedURL, err := url.Parse(tt.res.wantURL) + require.NoError(t, err) + actualURL, err := url.Parse(got) + require.NoError(t, err) + + // Compare the base parts (scheme, host, path) + assert.Equal(t, expectedURL.Scheme, actualURL.Scheme) + assert.Equal(t, expectedURL.Host, actualURL.Host) + assert.Equal(t, expectedURL.Path, actualURL.Path) + + // Compare the fragment if any + assert.Equal(t, expectedURL.Fragment, actualURL.Fragment) + + // For query parameters, compare them independently of order + expectedQuery := expectedURL.Query() + actualQuery := actualURL.Query() + + assert.Equal(t, len(expectedQuery), len(actualQuery), "Query parameter count does not match") + + for key, expectedValues := range expectedQuery { + actualValues, exists := actualQuery[key] + assert.True(t, exists, "Expected query parameter %s not found", key) + assert.ElementsMatch(t, expectedValues, actualValues, "Values for parameter %s don't match", key) + } + } + }) + } +} From 4f0ed79c0a49c9de7341300c0d1e45e5c1e38796 Mon Sep 17 00:00:00 2001 From: Ayato Date: Tue, 29 Apr 2025 23:33:31 +0900 Subject: [PATCH 489/502] fix(op): Add mitigation for PKCE Downgrade Attack (#741) * fix(op): Add mitigation for PKCE downgrade attack * chore(op): add test for PKCE verification --- pkg/op/token_code.go | 9 ++--- pkg/op/token_request.go | 12 +++++- pkg/op/token_request_test.go | 75 ++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 pkg/op/token_request_test.go diff --git a/pkg/op/token_code.go b/pkg/op/token_code.go index 019aa63..fb636b4 100644 --- a/pkg/op/token_code.go +++ b/pkg/op/token_code.go @@ -80,12 +80,9 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, } codeChallenge := request.GetCodeChallenge() - if codeChallenge != nil { - err = AuthorizeCodeChallenge(tokenReq.CodeVerifier, codeChallenge) - - if err != nil { - return nil, nil, err - } + err = AuthorizeCodeChallenge(tokenReq.CodeVerifier, codeChallenge) + if err != nil { + return nil, nil, err } if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion { diff --git a/pkg/op/token_request.go b/pkg/op/token_request.go index 85e2270..66e4c83 100644 --- a/pkg/op/token_request.go +++ b/pkg/op/token_request.go @@ -132,11 +132,19 @@ func AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string, // AuthorizeCodeChallenge authorizes a client by validating the code_verifier against the previously sent // code_challenge of the auth request (PKCE) func AuthorizeCodeChallenge(codeVerifier string, challenge *oidc.CodeChallenge) error { + if challenge == nil { + if codeVerifier != "" { + return oidc.ErrInvalidRequest().WithDescription("code_verifier unexpectedly provided") + } + + return nil + } + if codeVerifier == "" { - return oidc.ErrInvalidRequest().WithDescription("code_challenge required") + return oidc.ErrInvalidRequest().WithDescription("code_verifier required") } if !oidc.VerifyCodeChallenge(challenge, codeVerifier) { - return oidc.ErrInvalidGrant().WithDescription("invalid code challenge") + return oidc.ErrInvalidGrant().WithDescription("invalid code_verifier") } return nil } diff --git a/pkg/op/token_request_test.go b/pkg/op/token_request_test.go new file mode 100644 index 0000000..21cf20b --- /dev/null +++ b/pkg/op/token_request_test.go @@ -0,0 +1,75 @@ +package op_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" +) + +func TestAuthorizeCodeChallenge(t *testing.T) { + tests := []struct { + name string + codeVerifier string + codeChallenge *oidc.CodeChallenge + want func(t *testing.T, err error) + }{ + { + name: "missing both code_verifier and code_challenge", + codeVerifier: "", + codeChallenge: nil, + want: func(t *testing.T, err error) { + assert.Nil(t, err) + }, + }, + { + name: "valid code_verifier", + codeVerifier: "Hello World!", + codeChallenge: &oidc.CodeChallenge{ + Challenge: "f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", + Method: oidc.CodeChallengeMethodS256, + }, + want: func(t *testing.T, err error) { + assert.Nil(t, err) + }, + }, + { + name: "invalid code_verifier", + codeVerifier: "Hi World!", + codeChallenge: &oidc.CodeChallenge{ + Challenge: "f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", + Method: oidc.CodeChallengeMethodS256, + }, + want: func(t *testing.T, err error) { + assert.ErrorContains(t, err, "invalid code_verifier") + }, + }, + { + name: "code_verifier provided without code_challenge", + codeVerifier: "code_verifier", + codeChallenge: nil, + want: func(t *testing.T, err error) { + assert.ErrorContains(t, err, "code_verifier unexpectedly provided") + }, + }, + { + name: "empty code_verifier", + codeVerifier: "", + codeChallenge: &oidc.CodeChallenge{ + Challenge: "f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", + Method: oidc.CodeChallengeMethodS256, + }, + want: func(t *testing.T, err error) { + assert.ErrorContains(t, err, "code_verifier required") + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := op.AuthorizeCodeChallenge(tt.codeVerifier, tt.codeChallenge) + + tt.want(t, err) + }) + } +} From 4ed4d257ab4b4c3b1b2ffbdb7e41ffa88e3173ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 08:00:26 +0200 Subject: [PATCH 490/502] chore(deps): bump golang.org/x/oauth2 from 0.29.0 to 0.30.0 (#743) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.29.0 to 0.30.0. - [Commits](https://github.com/golang/oauth2/compare/v0.29.0...v0.30.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-version: 0.30.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f5ad96b..efb6b22 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/zitadel/logging v0.6.2 github.com/zitadel/schema v1.3.1 go.opentelemetry.io/otel v1.29.0 - golang.org/x/oauth2 v0.29.0 + golang.org/x/oauth2 v0.30.0 golang.org/x/text v0.24.0 ) diff --git a/go.sum b/go.sum index e0ac4f5..6507dbc 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= -golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= From 668fb0d37a2ae6b2b29706b3531c32681d00134f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 08:04:53 +0200 Subject: [PATCH 491/502] chore(deps): bump golang.org/x/text from 0.24.0 to 0.25.0 (#742) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.24.0 to 0.25.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.24.0...v0.25.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-version: 0.25.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index efb6b22..a15fd33 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/zitadel/schema v1.3.1 go.opentelemetry.io/otel v1.29.0 golang.org/x/oauth2 v0.30.0 - golang.org/x/text v0.24.0 + golang.org/x/text v0.25.0 ) require ( diff --git a/go.sum b/go.sum index 6507dbc..2f9e525 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,8 @@ golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= From 7d57aaa99983368527b0fc93f72ab5542b2eefd2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 15:22:02 +0300 Subject: [PATCH 492/502] chore(deps): bump codecov/codecov-action from 5.4.2 to 5.4.3 (#751) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.4.2 to 5.4.3. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v5.4.2...v5.4.3) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: 5.4.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 20cb6df..00063e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - - uses: codecov/codecov-action@v5.4.2 + - uses: codecov/codecov-action@v5.4.3 with: file: ./profile.cov name: codecov-go From f94bd541d7b78276261f7c453e69a9de71c1a6cd Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Thu, 5 Jun 2025 13:19:51 +0200 Subject: [PATCH 493/502] feat: update end session request to pass all params according to specification (#754) * feat: update end session request to pass all params according to specification * register encoder --- pkg/oidc/session.go | 12 +++++++----- pkg/oidc/types.go | 11 +++++++++++ pkg/op/session.go | 2 ++ pkg/op/storage.go | 3 +++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/pkg/oidc/session.go b/pkg/oidc/session.go index b470d1e..39f9f08 100644 --- a/pkg/oidc/session.go +++ b/pkg/oidc/session.go @@ -1,10 +1,12 @@ package oidc // EndSessionRequest for the RP-Initiated Logout according to: -//https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout +// https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout type EndSessionRequest struct { - IdTokenHint string `schema:"id_token_hint"` - ClientID string `schema:"client_id"` - PostLogoutRedirectURI string `schema:"post_logout_redirect_uri"` - State string `schema:"state"` + IdTokenHint string `schema:"id_token_hint"` + LogoutHint string `schema:"logout_hint"` + ClientID string `schema:"client_id"` + PostLogoutRedirectURI string `schema:"post_logout_redirect_uri"` + State string `schema:"state"` + UILocales Locales `schema:"ui_locales"` } diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index 9b307bc..33ad2d5 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -115,6 +115,14 @@ func ParseLocales(locales []string) Locales { return out } +func (l Locales) String() string { + tags := make([]string, len(l)) + for i, tag := range l { + tags[i] = tag.String() + } + return strings.Join(tags, " ") +} + // UnmarshalText implements the [encoding.TextUnmarshaler] interface. // It decodes an unquoted space seperated string into Locales. // Undefined language tags in the input are ignored and ommited from @@ -231,6 +239,9 @@ func NewEncoder() *schema.Encoder { e.RegisterEncoder(SpaceDelimitedArray{}, func(value reflect.Value) string { return value.Interface().(SpaceDelimitedArray).String() }) + e.RegisterEncoder(Locales{}, func(value reflect.Value) string { + return value.Interface().(Locales).String() + }) return e } diff --git a/pkg/op/session.go b/pkg/op/session.go index 8ac530d..eb67b3c 100644 --- a/pkg/op/session.go +++ b/pkg/op/session.go @@ -73,6 +73,8 @@ func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest, session := &EndSessionRequest{ RedirectURI: ender.DefaultLogoutRedirectURI(), + LogoutHint: req.LogoutHint, + UILocales: req.UILocales, } if req.IdTokenHint != "" { claims, err := VerifyIDTokenHint[*oidc.IDTokenClaims](ctx, req.IdTokenHint, ender.IDTokenHintVerifier(ctx)) diff --git a/pkg/op/storage.go b/pkg/op/storage.go index a579810..35d7040 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -6,6 +6,7 @@ import ( "time" jose "github.com/go-jose/go-jose/v4" + "golang.org/x/text/language" "github.com/zitadel/oidc/v3/pkg/oidc" ) @@ -170,6 +171,8 @@ type EndSessionRequest struct { ClientID string IDTokenHintClaims *oidc.IDTokenClaims RedirectURI string + LogoutHint string + UILocales []language.Tag } var ErrDuplicateUserCode = errors.New("user code already exists") From e1415ef2f35cd1204fd8c75a4cd54c9e92cf732b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Jun 2025 09:50:55 +0200 Subject: [PATCH 494/502] chore(deps): bump golang.org/x/text from 0.25.0 to 0.26.0 (#755) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.25.0 to 0.26.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.25.0...v0.26.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-version: 0.26.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a15fd33..33db99e 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/zitadel/schema v1.3.1 go.opentelemetry.io/otel v1.29.0 golang.org/x/oauth2 v0.30.0 - golang.org/x/text v0.25.0 + golang.org/x/text v0.26.0 ) require ( diff --git a/go.sum b/go.sum index 2f9e525..4835505 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,8 @@ golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= From e127c66db27199e97afbd32ad60f92943c9e0959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabienne=20B=C3=BChler?= Date: Tue, 17 Jun 2025 11:14:09 +0200 Subject: [PATCH 495/502] chore: update issue templates --- .github/ISSUE_TEMPLATE/bug_report.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 92465f9..d024341 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -2,6 +2,7 @@ name: Bug Report description: "Create a bug report to help us improve ZITADEL. Click [here](https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#product-management) to see how we process your issue." title: "[Bug]: " labels: ["bug"] +type: Bug body: - type: markdown attributes: From 187878de630e1fc464b8fd9f611806c94d0f30ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabienne=20B=C3=BChler?= Date: Tue, 17 Jun 2025 11:15:26 +0200 Subject: [PATCH 496/502] update docs issue template, add type --- .github/ISSUE_TEMPLATE/docs.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/docs.yaml b/.github/ISSUE_TEMPLATE/docs.yaml index 04c1c0c..d3f82b9 100644 --- a/.github/ISSUE_TEMPLATE/docs.yaml +++ b/.github/ISSUE_TEMPLATE/docs.yaml @@ -1,6 +1,7 @@ name: 📄 Documentation description: Create an issue for missing or wrong documentation. labels: ["docs"] +type: task body: - type: markdown attributes: From 5618487a883698d04b3d9177871c53d549b0f688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabienne=20B=C3=BChler?= Date: Tue, 17 Jun 2025 11:16:34 +0200 Subject: [PATCH 497/502] Update and rename improvement.yaml to enhancement.yaml --- .../ISSUE_TEMPLATE/{improvement.yaml => enhancement.yaml} | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) rename .github/ISSUE_TEMPLATE/{improvement.yaml => enhancement.yaml} (92%) diff --git a/.github/ISSUE_TEMPLATE/improvement.yaml b/.github/ISSUE_TEMPLATE/enhancement.yaml similarity index 92% rename from .github/ISSUE_TEMPLATE/improvement.yaml rename to .github/ISSUE_TEMPLATE/enhancement.yaml index 2e2ddf4..ef2103e 100644 --- a/.github/ISSUE_TEMPLATE/improvement.yaml +++ b/.github/ISSUE_TEMPLATE/enhancement.yaml @@ -1,11 +1,12 @@ name: 🛠️ Improvement description: "Create an new issue for an improvment in ZITADEL" -labels: ["improvement"] +labels: ["enhancement"] +type: enhancement body: - type: markdown attributes: value: | - Thanks for taking the time to fill out this improvement request + Thanks for taking the time to fill out this proposal / feature reqeust - type: checkboxes id: preflight attributes: From 8e1e5174fd3c6b7c350ba0e0fddf21146fdbc691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabienne=20B=C3=BChler?= Date: Tue, 17 Jun 2025 11:17:14 +0200 Subject: [PATCH 498/502] Delete .github/ISSUE_TEMPLATE/proposal.yaml --- .github/ISSUE_TEMPLATE/proposal.yaml | 44 ---------------------------- 1 file changed, 44 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/proposal.yaml diff --git a/.github/ISSUE_TEMPLATE/proposal.yaml b/.github/ISSUE_TEMPLATE/proposal.yaml deleted file mode 100644 index af7acd5..0000000 --- a/.github/ISSUE_TEMPLATE/proposal.yaml +++ /dev/null @@ -1,44 +0,0 @@ -name: 💡 Proposal / Feature request -description: "Create an issue for a feature request/proposal." -labels: ["enhancement"] -body: - - type: markdown - attributes: - value: | - Thanks for taking the time to fill out this proposal / feature reqeust - - type: checkboxes - id: preflight - attributes: - label: Preflight Checklist - options: - - label: - I could not find a solution in the existing issues, docs, nor discussions - required: true - - label: - I have joined the [ZITADEL chat](https://zitadel.com/chat) - - type: textarea - id: problem - attributes: - label: Describe your problem - description: Please describe your problem this proposal / feature is supposed to solve. - placeholder: Describe the problem you have. - validations: - required: true - - type: textarea - id: solution - attributes: - label: Describe your ideal solution - description: Which solution do you propose? - placeholder: As a [type of user], I want [some goal] so that [some reason]. - validations: - required: true - - type: input - id: version - attributes: - label: Version - description: Which version of the OIDC Library are you using. - - type: textarea - id: additional - attributes: - label: Additional Context - description: Please add any other infos that could be useful. From 154fbe642027fb8845bb9d8f35fd0403f8dd0711 Mon Sep 17 00:00:00 2001 From: "ORZ (Paul Orzel)" Date: Fri, 20 Jun 2025 08:44:27 +0200 Subject: [PATCH 499/502] Revert "feat(op): always verify code challenge when available (#721)" Breaks OIDC for some not yet updated applications, that we use. This reverts commit c51628ea27035796152a32631439625b55f0a7ea. --- example/client/app/app.go | 12 ---------- example/server/exampleop/templates/login.html | 4 ++-- example/server/storage/oidc.go | 15 ++++-------- pkg/op/op_test.go | 1 - pkg/op/server_http_routes_test.go | 2 +- pkg/op/token_code.go | 23 ++++++------------- 6 files changed, 15 insertions(+), 42 deletions(-) diff --git a/example/client/app/app.go b/example/client/app/app.go index 5740591..0b9b19d 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -7,7 +7,6 @@ import ( "log/slog" "net/http" "os" - "strconv" "strings" "sync/atomic" "time" @@ -35,14 +34,6 @@ func main() { scopes := strings.Split(os.Getenv("SCOPES"), " ") responseMode := os.Getenv("RESPONSE_MODE") - var pkce bool - if pkceEnv, ok := os.LookupEnv("PKCE"); ok { - var err error - pkce, err = strconv.ParseBool(pkceEnv) - if err != nil { - logrus.Fatalf("error parsing PKCE %s", err.Error()) - } - } redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath) cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure()) @@ -73,9 +64,6 @@ func main() { if keyPath != "" { options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath))) } - if pkce { - options = append(options, rp.WithPKCE(cookieHandler)) - } // One can add a logger to the context, // pre-defining log attributes as required. diff --git a/example/server/exampleop/templates/login.html b/example/server/exampleop/templates/login.html index d7f8f9a..b048211 100644 --- a/example/server/exampleop/templates/login.html +++ b/example/server/exampleop/templates/login.html @@ -25,5 +25,5 @@ - -{{- end }} +` +{{- end }} \ No newline at end of file diff --git a/example/server/storage/oidc.go b/example/server/storage/oidc.go index 3d5d86b..c04877f 100644 --- a/example/server/storage/oidc.go +++ b/example/server/storage/oidc.go @@ -18,7 +18,7 @@ const ( // CustomClaim is an example for how to return custom claims with this library CustomClaim = "custom_claim" - // CustomScopeImpersonatePrefix is an example scope prefix for passing user id to impersonate using token exchange + // CustomScopeImpersonatePrefix is an example scope prefix for passing user id to impersonate using token exchage CustomScopeImpersonatePrefix = "custom_scope:impersonate:" ) @@ -143,14 +143,6 @@ func MaxAgeToInternal(maxAge *uint) *time.Duration { } func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthRequest { - var codeChallenge *OIDCCodeChallenge - if authReq.CodeChallenge != "" { - codeChallenge = &OIDCCodeChallenge{ - Challenge: authReq.CodeChallenge, - Method: string(authReq.CodeChallengeMethod), - } - } - return &AuthRequest{ CreationDate: time.Now(), ApplicationID: authReq.ClientID, @@ -165,7 +157,10 @@ func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthReques ResponseType: authReq.ResponseType, ResponseMode: authReq.ResponseMode, Nonce: authReq.Nonce, - CodeChallenge: codeChallenge, + CodeChallenge: &OIDCCodeChallenge{ + Challenge: authReq.CodeChallenge, + Method: string(authReq.CodeChallengeMethod), + }, } } diff --git a/pkg/op/op_test.go b/pkg/op/op_test.go index c1520e2..9a4a624 100644 --- a/pkg/op/op_test.go +++ b/pkg/op/op_test.go @@ -102,7 +102,6 @@ func TestRoutes(t *testing.T) { authReq, err := storage.CreateAuthRequest(ctx, oidcAuthReq, "id1") require.NoError(t, err) storage.AuthRequestDone(authReq.GetID()) - storage.SaveAuthCode(ctx, authReq.GetID(), "123") accessToken, refreshToken, _, err := op.CreateAccessToken(ctx, authReq, op.AccessTokenTypeBearer, testProvider, client, "") require.NoError(t, err) diff --git a/pkg/op/server_http_routes_test.go b/pkg/op/server_http_routes_test.go index e0e4a97..1bfb32b 100644 --- a/pkg/op/server_http_routes_test.go +++ b/pkg/op/server_http_routes_test.go @@ -130,7 +130,7 @@ func TestServerRoutes(t *testing.T) { "client_id": client.GetID(), "client_secret": "secret", "redirect_uri": "https://example.com", - "code": "abc", + "code": "123", }, wantCode: http.StatusBadRequest, json: `{"error":"invalid_grant", "error_description":"invalid code"}`, diff --git a/pkg/op/token_code.go b/pkg/op/token_code.go index fb636b4..3612240 100644 --- a/pkg/op/token_code.go +++ b/pkg/op/token_code.go @@ -74,17 +74,6 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, ctx, span := tracer.Start(ctx, "AuthorizeCodeClient") defer span.End() - request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code) - if err != nil { - return nil, nil, err - } - - codeChallenge := request.GetCodeChallenge() - err = AuthorizeCodeChallenge(tokenReq.CodeVerifier, codeChallenge) - if err != nil { - return nil, nil, err - } - if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion { jwtExchanger, ok := exchanger.(JWTAuthorizationGrantExchanger) if !ok || !exchanger.AuthMethodPrivateKeyJWTSupported() { @@ -94,9 +83,9 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, if err != nil { return nil, nil, err } + request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code) return request, client, err } - client, err = exchanger.Storage().GetClientByClientID(ctx, tokenReq.ClientID) if err != nil { return nil, nil, oidc.ErrInvalidClient().WithParent(err) @@ -105,10 +94,12 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, return nil, nil, oidc.ErrInvalidClient().WithDescription("private_key_jwt not allowed for this client") } if client.AuthMethod() == oidc.AuthMethodNone { - if codeChallenge == nil { - return nil, nil, oidc.ErrInvalidRequest().WithDescription("PKCE required") + request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code) + if err != nil { + return nil, nil, err } - return request, client, nil + err = AuthorizeCodeChallenge(tokenReq.CodeVerifier, request.GetCodeChallenge()) + return request, client, err } if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() { return nil, nil, oidc.ErrInvalidClient().WithDescription("auth_method post not supported") @@ -117,7 +108,7 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, if err != nil { return nil, nil, err } - + request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code) return request, client, err } From 53c4d07b450b2e4fe587d3a8a4ff8d03c4bc19ca Mon Sep 17 00:00:00 2001 From: "ORZ (Paul Orzel)" Date: Fri, 20 Jun 2025 08:56:29 +0200 Subject: [PATCH 500/502] remove actions --- {.github => .forgejo.bak}/ISSUE_TEMPLATE/bug_report.yaml | 0 {.github => .forgejo.bak}/ISSUE_TEMPLATE/config.yml | 0 {.github => .forgejo.bak}/ISSUE_TEMPLATE/docs.yaml | 0 {.github => .forgejo.bak}/ISSUE_TEMPLATE/enhancement.yaml | 0 {.github => .forgejo.bak}/dependabot.yml | 0 {.github => .forgejo.bak}/pull_request_template.md | 0 {.github => .forgejo.bak}/workflows/codeql-analysis.yml | 0 {.github => .forgejo.bak}/workflows/issue.yml | 0 {.github => .forgejo.bak}/workflows/release.yml | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename {.github => .forgejo.bak}/ISSUE_TEMPLATE/bug_report.yaml (100%) rename {.github => .forgejo.bak}/ISSUE_TEMPLATE/config.yml (100%) rename {.github => .forgejo.bak}/ISSUE_TEMPLATE/docs.yaml (100%) rename {.github => .forgejo.bak}/ISSUE_TEMPLATE/enhancement.yaml (100%) rename {.github => .forgejo.bak}/dependabot.yml (100%) rename {.github => .forgejo.bak}/pull_request_template.md (100%) rename {.github => .forgejo.bak}/workflows/codeql-analysis.yml (100%) rename {.github => .forgejo.bak}/workflows/issue.yml (100%) rename {.github => .forgejo.bak}/workflows/release.yml (100%) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.forgejo.bak/ISSUE_TEMPLATE/bug_report.yaml similarity index 100% rename from .github/ISSUE_TEMPLATE/bug_report.yaml rename to .forgejo.bak/ISSUE_TEMPLATE/bug_report.yaml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.forgejo.bak/ISSUE_TEMPLATE/config.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/config.yml rename to .forgejo.bak/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/docs.yaml b/.forgejo.bak/ISSUE_TEMPLATE/docs.yaml similarity index 100% rename from .github/ISSUE_TEMPLATE/docs.yaml rename to .forgejo.bak/ISSUE_TEMPLATE/docs.yaml diff --git a/.github/ISSUE_TEMPLATE/enhancement.yaml b/.forgejo.bak/ISSUE_TEMPLATE/enhancement.yaml similarity index 100% rename from .github/ISSUE_TEMPLATE/enhancement.yaml rename to .forgejo.bak/ISSUE_TEMPLATE/enhancement.yaml diff --git a/.github/dependabot.yml b/.forgejo.bak/dependabot.yml similarity index 100% rename from .github/dependabot.yml rename to .forgejo.bak/dependabot.yml diff --git a/.github/pull_request_template.md b/.forgejo.bak/pull_request_template.md similarity index 100% rename from .github/pull_request_template.md rename to .forgejo.bak/pull_request_template.md diff --git a/.github/workflows/codeql-analysis.yml b/.forgejo.bak/workflows/codeql-analysis.yml similarity index 100% rename from .github/workflows/codeql-analysis.yml rename to .forgejo.bak/workflows/codeql-analysis.yml diff --git a/.github/workflows/issue.yml b/.forgejo.bak/workflows/issue.yml similarity index 100% rename from .github/workflows/issue.yml rename to .forgejo.bak/workflows/issue.yml diff --git a/.github/workflows/release.yml b/.forgejo.bak/workflows/release.yml similarity index 100% rename from .github/workflows/release.yml rename to .forgejo.bak/workflows/release.yml From 29d69ca2e0eabe4c9e19c37bc48a0f3788ee661f Mon Sep 17 00:00:00 2001 From: "ORZ (Paul Orzel)" Date: Fri, 20 Jun 2025 09:39:40 +0200 Subject: [PATCH 501/502] add function to marshal aud into a string if the array has a len of 1, to comply with rfc --- pkg/oidc/types.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index 33ad2d5..5d063b1 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -35,6 +35,17 @@ func (a *Audience) UnmarshalJSON(text []byte) error { return nil } +func (a *Audience) MarshalJSON() ([]byte, error) { + len := len(*a) + if len > 1 { + return json.Marshal(*a) + } else if len == 1 { + return json.Marshal((*a)[0]) + } + + return nil, errors.New("aud is empty") +} + type Display string func (d *Display) UnmarshalText(text []byte) error { From 653b807f5db15b4872bb8db1aca80ca004a6127d Mon Sep 17 00:00:00 2001 From: "ORZ (Paul Orzel)" Date: Fri, 20 Jun 2025 09:45:28 +0200 Subject: [PATCH 502/502] replace github url --- example/client/api/api.go | 4 ++-- example/client/app/app.go | 6 ++--- example/client/device/device.go | 4 ++-- example/client/github/github.go | 8 +++---- example/client/service/service.go | 2 +- example/server/dynamic/login.go | 2 +- example/server/dynamic/op.go | 4 ++-- example/server/exampleop/device.go | 2 +- example/server/exampleop/login.go | 2 +- example/server/exampleop/op.go | 2 +- example/server/main.go | 6 ++--- example/server/storage/client.go | 4 ++-- example/server/storage/oidc.go | 4 ++-- example/server/storage/storage.go | 4 ++-- example/server/storage/storage_dynamic.go | 4 ++-- go.mod | 2 +- internal/testutil/gen/gen.go | 4 ++-- internal/testutil/token.go | 2 +- pkg/client/client.go | 6 ++--- pkg/client/client_test.go | 2 +- pkg/client/integration_test.go | 16 ++++++------- pkg/client/jwt_profile.go | 4 ++-- pkg/client/profile/jwt_profile.go | 4 ++-- pkg/client/rp/cli/cli.go | 6 ++--- pkg/client/rp/delegation.go | 2 +- pkg/client/rp/device.go | 4 ++-- pkg/client/rp/jwks.go | 6 ++--- pkg/client/rp/relying_party.go | 6 ++--- pkg/client/rp/relying_party_test.go | 4 ++-- pkg/client/rp/tockenexchange.go | 2 +- pkg/client/rp/userinfo_example_test.go | 4 ++-- pkg/client/rp/verifier.go | 4 ++-- pkg/client/rp/verifier_test.go | 4 ++-- pkg/client/rp/verifier_tokens_example_test.go | 6 ++--- pkg/client/rs/introspect_example_test.go | 4 ++-- pkg/client/rs/resource_server.go | 6 ++--- pkg/client/rs/resource_server_test.go | 2 +- pkg/client/tokenexchange/tokenexchange.go | 6 ++--- pkg/crypto/key_test.go | 2 +- pkg/http/http.go | 2 +- pkg/oidc/code_challenge.go | 2 +- pkg/oidc/token.go | 2 +- pkg/oidc/verifier_parse_test.go | 4 ++-- pkg/op/auth_request.go | 4 ++-- pkg/op/auth_request_test.go | 12 +++++----- pkg/op/client.go | 4 ++-- pkg/op/client_test.go | 8 +++---- pkg/op/crypto.go | 2 +- pkg/op/device.go | 4 ++-- pkg/op/device_test.go | 6 ++--- pkg/op/discovery.go | 4 ++-- pkg/op/discovery_test.go | 6 ++--- pkg/op/endpoint_test.go | 2 +- pkg/op/error.go | 4 ++-- pkg/op/error_test.go | 2 +- pkg/op/keys.go | 2 +- pkg/op/keys_test.go | 6 ++--- pkg/op/mock/authorizer.mock.go | 6 ++--- pkg/op/mock/authorizer.mock.impl.go | 4 ++-- pkg/op/mock/client.go | 4 ++-- pkg/op/mock/client.mock.go | 6 ++--- pkg/op/mock/configuration.mock.go | 4 ++-- pkg/op/mock/discovery.mock.go | 2 +- pkg/op/mock/generate.go | 16 ++++++------- pkg/op/mock/glob.go | 4 ++-- pkg/op/mock/glob.mock.go | 6 ++--- pkg/op/mock/key.mock.go | 4 ++-- pkg/op/mock/signer.mock.go | 2 +- pkg/op/mock/storage.mock.go | 6 ++--- pkg/op/mock/storage.mock.impl.go | 4 ++-- pkg/op/op.go | 4 ++-- pkg/op/op_test.go | 6 ++--- pkg/op/probes.go | 2 +- pkg/op/server.go | 4 ++-- pkg/op/server_http.go | 4 ++-- pkg/op/server_http_routes_test.go | 6 ++--- pkg/op/server_http_test.go | 4 ++-- pkg/op/server_legacy.go | 2 +- pkg/op/session.go | 4 ++-- pkg/op/storage.go | 2 +- pkg/op/token.go | 4 ++-- pkg/op/token_client_credentials.go | 4 ++-- pkg/op/token_code.go | 4 ++-- pkg/op/token_exchange.go | 4 ++-- pkg/op/token_intospection.go | 4 ++-- pkg/op/token_jwt_profile.go | 4 ++-- pkg/op/token_refresh.go | 4 ++-- pkg/op/token_request.go | 4 ++-- pkg/op/token_request_test.go | 24 +++++++++---------- pkg/op/token_revocation.go | 4 ++-- pkg/op/userinfo.go | 4 ++-- pkg/op/verifier_access_token.go | 2 +- pkg/op/verifier_access_token_example_test.go | 6 ++--- pkg/op/verifier_access_token_test.go | 4 ++-- pkg/op/verifier_id_token_hint.go | 2 +- pkg/op/verifier_id_token_hint_test.go | 4 ++-- pkg/op/verifier_jwt_profile.go | 2 +- pkg/op/verifier_jwt_profile_test.go | 6 ++--- 98 files changed, 219 insertions(+), 219 deletions(-) diff --git a/example/client/api/api.go b/example/client/api/api.go index 2e61c21..69f9466 100644 --- a/example/client/api/api.go +++ b/example/client/api/api.go @@ -13,8 +13,8 @@ import ( "github.com/go-chi/chi/v5" "github.com/sirupsen/logrus" - "github.com/zitadel/oidc/v3/pkg/client/rs" - "github.com/zitadel/oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rs" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) const ( diff --git a/example/client/app/app.go b/example/client/app/app.go index 0b9b19d..90b1969 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -14,10 +14,10 @@ import ( "github.com/google/uuid" "github.com/sirupsen/logrus" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v3/pkg/client/rp" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" ) var ( diff --git a/example/client/device/device.go b/example/client/device/device.go index 78ed2c8..33bc570 100644 --- a/example/client/device/device.go +++ b/example/client/device/device.go @@ -45,8 +45,8 @@ import ( "github.com/sirupsen/logrus" - "github.com/zitadel/oidc/v3/pkg/client/rp" - httphelper "github.com/zitadel/oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" ) var ( diff --git a/example/client/github/github.go b/example/client/github/github.go index 7d069d4..f6c536b 100644 --- a/example/client/github/github.go +++ b/example/client/github/github.go @@ -10,10 +10,10 @@ import ( "golang.org/x/oauth2" githubOAuth "golang.org/x/oauth2/github" - "github.com/zitadel/oidc/v3/pkg/client/rp" - "github.com/zitadel/oidc/v3/pkg/client/rp/cli" - "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp/cli" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) var ( diff --git a/example/client/service/service.go b/example/client/service/service.go index 865a4e0..a88ab2f 100644 --- a/example/client/service/service.go +++ b/example/client/service/service.go @@ -13,7 +13,7 @@ import ( "github.com/sirupsen/logrus" "golang.org/x/oauth2" - "github.com/zitadel/oidc/v3/pkg/client/profile" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/profile" ) var client = http.DefaultClient diff --git a/example/server/dynamic/login.go b/example/server/dynamic/login.go index 685b444..05f0e34 100644 --- a/example/server/dynamic/login.go +++ b/example/server/dynamic/login.go @@ -8,7 +8,7 @@ import ( "github.com/go-chi/chi/v5" - "github.com/zitadel/oidc/v3/pkg/op" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" ) const ( diff --git a/example/server/dynamic/op.go b/example/server/dynamic/op.go index 432a575..2c00e41 100644 --- a/example/server/dynamic/op.go +++ b/example/server/dynamic/op.go @@ -10,8 +10,8 @@ import ( "github.com/go-chi/chi/v5" "golang.org/x/text/language" - "github.com/zitadel/oidc/v3/example/server/storage" - "github.com/zitadel/oidc/v3/pkg/op" + "git.christmann.info/LARA/zitadel-oidc/v3/example/server/storage" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" ) const ( diff --git a/example/server/exampleop/device.go b/example/server/exampleop/device.go index 2f9be52..99505e4 100644 --- a/example/server/exampleop/device.go +++ b/example/server/exampleop/device.go @@ -8,10 +8,10 @@ import ( "net/http" "net/url" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" "github.com/go-chi/chi/v5" "github.com/gorilla/securecookie" "github.com/sirupsen/logrus" - "github.com/zitadel/oidc/v3/pkg/op" ) type deviceAuthenticate interface { diff --git a/example/server/exampleop/login.go b/example/server/exampleop/login.go index 4d2b478..77a6189 100644 --- a/example/server/exampleop/login.go +++ b/example/server/exampleop/login.go @@ -5,8 +5,8 @@ import ( "fmt" "net/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" "github.com/go-chi/chi/v5" - "github.com/zitadel/oidc/v3/pkg/op" ) type login struct { diff --git a/example/server/exampleop/op.go b/example/server/exampleop/op.go index 8f55b0a..e12c755 100644 --- a/example/server/exampleop/op.go +++ b/example/server/exampleop/op.go @@ -12,7 +12,7 @@ import ( "github.com/zitadel/logging" "golang.org/x/text/language" - "github.com/zitadel/oidc/v3/pkg/op" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" ) const ( diff --git a/example/server/main.go b/example/server/main.go index 6d345e1..5bdbb05 100644 --- a/example/server/main.go +++ b/example/server/main.go @@ -6,9 +6,9 @@ import ( "net/http" "os" - "github.com/zitadel/oidc/v3/example/server/config" - "github.com/zitadel/oidc/v3/example/server/exampleop" - "github.com/zitadel/oidc/v3/example/server/storage" + "git.christmann.info/LARA/zitadel-oidc/v3/example/server/config" + "git.christmann.info/LARA/zitadel-oidc/v3/example/server/exampleop" + "git.christmann.info/LARA/zitadel-oidc/v3/example/server/storage" ) func getUserStore(cfg *config.Config) (storage.UserStore, error) { diff --git a/example/server/storage/client.go b/example/server/storage/client.go index 010b9ce..2b836c0 100644 --- a/example/server/storage/client.go +++ b/example/server/storage/client.go @@ -3,8 +3,8 @@ package storage import ( "time" - "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/op" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" ) var ( diff --git a/example/server/storage/oidc.go b/example/server/storage/oidc.go index c04877f..9c7f544 100644 --- a/example/server/storage/oidc.go +++ b/example/server/storage/oidc.go @@ -6,8 +6,8 @@ import ( "golang.org/x/text/language" - "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/op" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" ) const ( diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index fee34c5..d4315c6 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -14,8 +14,8 @@ import ( jose "github.com/go-jose/go-jose/v4" "github.com/google/uuid" - "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/op" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" ) // serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant diff --git a/example/server/storage/storage_dynamic.go b/example/server/storage/storage_dynamic.go index d112d71..765d29a 100644 --- a/example/server/storage/storage_dynamic.go +++ b/example/server/storage/storage_dynamic.go @@ -6,8 +6,8 @@ import ( jose "github.com/go-jose/go-jose/v4" - "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/op" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" ) type multiStorage struct { diff --git a/go.mod b/go.mod index 33db99e..a0f42c4 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/zitadel/oidc/v3 +module git.christmann.info/LARA/zitadel-oidc/v3 go 1.23.7 diff --git a/internal/testutil/gen/gen.go b/internal/testutil/gen/gen.go index e4a5718..3e44b7d 100644 --- a/internal/testutil/gen/gen.go +++ b/internal/testutil/gen/gen.go @@ -8,8 +8,8 @@ import ( "fmt" "os" - tu "github.com/zitadel/oidc/v3/internal/testutil" - "github.com/zitadel/oidc/v3/pkg/oidc" + tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) var custom = map[string]any{ diff --git a/internal/testutil/token.go b/internal/testutil/token.go index 7ad8893..72d08c5 100644 --- a/internal/testutil/token.go +++ b/internal/testutil/token.go @@ -8,9 +8,9 @@ import ( "errors" "time" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" jose "github.com/go-jose/go-jose/v4" "github.com/muhlemmer/gu" - "github.com/zitadel/oidc/v3/pkg/oidc" ) // KeySet implements oidc.Keys diff --git a/pkg/client/client.go b/pkg/client/client.go index 56417b5..2e1f536 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -15,9 +15,9 @@ import ( "go.opentelemetry.io/otel" "golang.org/x/oauth2" - "github.com/zitadel/oidc/v3/pkg/crypto" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) var ( diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 1046941..9e21e8e 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -5,9 +5,9 @@ import ( "net/http" "testing" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v3/pkg/oidc" ) func TestDiscover(t *testing.T) { diff --git a/pkg/client/integration_test.go b/pkg/client/integration_test.go index 98a9d3a..86a9ab7 100644 --- a/pkg/client/integration_test.go +++ b/pkg/client/integration_test.go @@ -23,14 +23,14 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/oauth2" - "github.com/zitadel/oidc/v3/example/server/exampleop" - "github.com/zitadel/oidc/v3/example/server/storage" - "github.com/zitadel/oidc/v3/pkg/client/rp" - "github.com/zitadel/oidc/v3/pkg/client/rs" - "github.com/zitadel/oidc/v3/pkg/client/tokenexchange" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/op" + "git.christmann.info/LARA/zitadel-oidc/v3/example/server/exampleop" + "git.christmann.info/LARA/zitadel-oidc/v3/example/server/storage" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rs" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/tokenexchange" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" ) var Logger = slog.New( diff --git a/pkg/client/jwt_profile.go b/pkg/client/jwt_profile.go index 0a5d9ec..98a54fd 100644 --- a/pkg/client/jwt_profile.go +++ b/pkg/client/jwt_profile.go @@ -6,8 +6,8 @@ import ( "golang.org/x/oauth2" - "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) // JWTProfileExchange handles the oauth2 jwt profile exchange diff --git a/pkg/client/profile/jwt_profile.go b/pkg/client/profile/jwt_profile.go index 060f390..fb351f0 100644 --- a/pkg/client/profile/jwt_profile.go +++ b/pkg/client/profile/jwt_profile.go @@ -8,8 +8,8 @@ import ( jose "github.com/go-jose/go-jose/v4" "golang.org/x/oauth2" - "github.com/zitadel/oidc/v3/pkg/client" - "github.com/zitadel/oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) type TokenSource interface { diff --git a/pkg/client/rp/cli/cli.go b/pkg/client/rp/cli/cli.go index eeb9011..10edaa7 100644 --- a/pkg/client/rp/cli/cli.go +++ b/pkg/client/rp/cli/cli.go @@ -4,9 +4,9 @@ import ( "context" "net/http" - "github.com/zitadel/oidc/v3/pkg/client/rp" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) const ( diff --git a/pkg/client/rp/delegation.go b/pkg/client/rp/delegation.go index 23ecffd..fb4fc63 100644 --- a/pkg/client/rp/delegation.go +++ b/pkg/client/rp/delegation.go @@ -1,7 +1,7 @@ package rp import ( - "github.com/zitadel/oidc/v3/pkg/oidc/grants/tokenexchange" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc/grants/tokenexchange" ) // DelegationTokenRequest is an implementation of TokenExchangeRequest diff --git a/pkg/client/rp/device.go b/pkg/client/rp/device.go index c2d1f8a..1fadd56 100644 --- a/pkg/client/rp/device.go +++ b/pkg/client/rp/device.go @@ -5,8 +5,8 @@ import ( "fmt" "time" - "github.com/zitadel/oidc/v3/pkg/client" - "github.com/zitadel/oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.ClientCredentialsRequest, error) { diff --git a/pkg/client/rp/jwks.go b/pkg/client/rp/jwks.go index c44a267..0ccbad2 100644 --- a/pkg/client/rp/jwks.go +++ b/pkg/client/rp/jwks.go @@ -9,9 +9,9 @@ import ( jose "github.com/go-jose/go-jose/v4" - "github.com/zitadel/oidc/v3/pkg/client" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) func NewRemoteKeySet(client *http.Client, jwksURL string, opts ...func(*remoteKeySet)) oidc.KeySet { diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index e6fa078..c2759a2 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -14,10 +14,10 @@ import ( "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v3/pkg/client" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" ) const ( diff --git a/pkg/client/rp/relying_party_test.go b/pkg/client/rp/relying_party_test.go index 4c5a1b3..b3bb6ee 100644 --- a/pkg/client/rp/relying_party_test.go +++ b/pkg/client/rp/relying_party_test.go @@ -5,10 +5,10 @@ import ( "testing" "time" + tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - tu "github.com/zitadel/oidc/v3/internal/testutil" - "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" ) diff --git a/pkg/client/rp/tockenexchange.go b/pkg/client/rp/tockenexchange.go index c8ca048..aa2cf99 100644 --- a/pkg/client/rp/tockenexchange.go +++ b/pkg/client/rp/tockenexchange.go @@ -5,7 +5,7 @@ import ( "golang.org/x/oauth2" - "github.com/zitadel/oidc/v3/pkg/oidc/grants/tokenexchange" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc/grants/tokenexchange" ) // TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange` diff --git a/pkg/client/rp/userinfo_example_test.go b/pkg/client/rp/userinfo_example_test.go index 2cc5222..78e014e 100644 --- a/pkg/client/rp/userinfo_example_test.go +++ b/pkg/client/rp/userinfo_example_test.go @@ -4,8 +4,8 @@ import ( "context" "fmt" - "github.com/zitadel/oidc/v3/pkg/client/rp" - "github.com/zitadel/oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) type UserInfo struct { diff --git a/pkg/client/rp/verifier.go b/pkg/client/rp/verifier.go index ca59454..0088b81 100644 --- a/pkg/client/rp/verifier.go +++ b/pkg/client/rp/verifier.go @@ -6,8 +6,8 @@ import ( jose "github.com/go-jose/go-jose/v4" - "github.com/zitadel/oidc/v3/pkg/client" - "github.com/zitadel/oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) // VerifyTokens implement the Token Response Validation as defined in OIDC specification diff --git a/pkg/client/rp/verifier_test.go b/pkg/client/rp/verifier_test.go index 24d35af..38f5a4a 100644 --- a/pkg/client/rp/verifier_test.go +++ b/pkg/client/rp/verifier_test.go @@ -5,11 +5,11 @@ import ( "testing" "time" + tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" jose "github.com/go-jose/go-jose/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - tu "github.com/zitadel/oidc/v3/internal/testutil" - "github.com/zitadel/oidc/v3/pkg/oidc" ) func TestVerifyTokens(t *testing.T) { diff --git a/pkg/client/rp/verifier_tokens_example_test.go b/pkg/client/rp/verifier_tokens_example_test.go index 892eb23..7ae68d6 100644 --- a/pkg/client/rp/verifier_tokens_example_test.go +++ b/pkg/client/rp/verifier_tokens_example_test.go @@ -4,9 +4,9 @@ import ( "context" "fmt" - tu "github.com/zitadel/oidc/v3/internal/testutil" - "github.com/zitadel/oidc/v3/pkg/client/rp" - "github.com/zitadel/oidc/v3/pkg/oidc" + tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) // MyCustomClaims extends the TokenClaims base, diff --git a/pkg/client/rs/introspect_example_test.go b/pkg/client/rs/introspect_example_test.go index eac8be2..1f67d11 100644 --- a/pkg/client/rs/introspect_example_test.go +++ b/pkg/client/rs/introspect_example_test.go @@ -4,8 +4,8 @@ import ( "context" "fmt" - "github.com/zitadel/oidc/v3/pkg/client/rs" - "github.com/zitadel/oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rs" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) type IntrospectionResponse struct { diff --git a/pkg/client/rs/resource_server.go b/pkg/client/rs/resource_server.go index 962af7e..993796e 100644 --- a/pkg/client/rs/resource_server.go +++ b/pkg/client/rs/resource_server.go @@ -6,9 +6,9 @@ import ( "net/http" "time" - "github.com/zitadel/oidc/v3/pkg/client" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) type ResourceServer interface { diff --git a/pkg/client/rs/resource_server_test.go b/pkg/client/rs/resource_server_test.go index 7a5ced9..afd7441 100644 --- a/pkg/client/rs/resource_server_test.go +++ b/pkg/client/rs/resource_server_test.go @@ -4,9 +4,9 @@ import ( "context" "testing" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v3/pkg/oidc" ) func TestNewResourceServer(t *testing.T) { diff --git a/pkg/client/tokenexchange/tokenexchange.go b/pkg/client/tokenexchange/tokenexchange.go index 61975a4..9cc1328 100644 --- a/pkg/client/tokenexchange/tokenexchange.go +++ b/pkg/client/tokenexchange/tokenexchange.go @@ -6,10 +6,10 @@ import ( "net/http" "time" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/go-jose/go-jose/v4" - "github.com/zitadel/oidc/v3/pkg/client" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" ) type TokenExchanger interface { diff --git a/pkg/crypto/key_test.go b/pkg/crypto/key_test.go index 8ed5cb5..a6fa493 100644 --- a/pkg/crypto/key_test.go +++ b/pkg/crypto/key_test.go @@ -10,7 +10,7 @@ import ( "github.com/go-jose/go-jose/v4" "github.com/stretchr/testify/assert" - zcrypto "github.com/zitadel/oidc/v3/pkg/crypto" + zcrypto "git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto" ) func TestBytesToPrivateKey(t *testing.T) { diff --git a/pkg/http/http.go b/pkg/http/http.go index 33c5f15..aa0ff6f 100644 --- a/pkg/http/http.go +++ b/pkg/http/http.go @@ -11,7 +11,7 @@ import ( "strings" "time" - "github.com/zitadel/oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) var DefaultHTTPClient = &http.Client{ diff --git a/pkg/oidc/code_challenge.go b/pkg/oidc/code_challenge.go index 3296362..0c593df 100644 --- a/pkg/oidc/code_challenge.go +++ b/pkg/oidc/code_challenge.go @@ -3,7 +3,7 @@ package oidc import ( "crypto/sha256" - "github.com/zitadel/oidc/v3/pkg/crypto" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto" ) const ( diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index d2b6f6d..4b43dcb 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -10,7 +10,7 @@ import ( "github.com/muhlemmer/gu" - "github.com/zitadel/oidc/v3/pkg/crypto" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto" ) const ( diff --git a/pkg/oidc/verifier_parse_test.go b/pkg/oidc/verifier_parse_test.go index 105650f..9cf5c1e 100644 --- a/pkg/oidc/verifier_parse_test.go +++ b/pkg/oidc/verifier_parse_test.go @@ -5,10 +5,10 @@ import ( "encoding/json" "testing" + tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - tu "github.com/zitadel/oidc/v3/internal/testutil" - "github.com/zitadel/oidc/v3/pkg/oidc" ) func TestParseToken(t *testing.T) { diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 2c013aa..b1434cc 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -15,9 +15,9 @@ import ( "strings" "time" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/bmatcuk/doublestar/v4" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" ) type AuthRequest interface { diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index f0c4ef1..d1ea965 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -11,15 +11,15 @@ import ( "reflect" "testing" + "git.christmann.info/LARA/zitadel-oidc/v3/example/server/storage" + tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op/mock" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v3/example/server/storage" - tu "github.com/zitadel/oidc/v3/internal/testutil" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/op" - "github.com/zitadel/oidc/v3/pkg/op/mock" "github.com/zitadel/schema" ) diff --git a/pkg/op/client.go b/pkg/op/client.go index 913944c..a4f44d3 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -7,8 +7,8 @@ import ( "net/url" "time" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) //go:generate go get github.com/dmarkham/enumer diff --git a/pkg/op/client_test.go b/pkg/op/client_test.go index b772ba5..b416630 100644 --- a/pkg/op/client_test.go +++ b/pkg/op/client_test.go @@ -10,13 +10,13 @@ import ( "strings" "testing" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op/mock" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/op" - "github.com/zitadel/oidc/v3/pkg/op/mock" "github.com/zitadel/schema" ) diff --git a/pkg/op/crypto.go b/pkg/op/crypto.go index 6ab1e0a..01aaad3 100644 --- a/pkg/op/crypto.go +++ b/pkg/op/crypto.go @@ -1,7 +1,7 @@ package op import ( - "github.com/zitadel/oidc/v3/pkg/crypto" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto" ) type Crypto interface { diff --git a/pkg/op/device.go b/pkg/op/device.go index b7290cd..866cbc4 100644 --- a/pkg/op/device.go +++ b/pkg/op/device.go @@ -13,8 +13,8 @@ import ( "strings" "time" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) type DeviceAuthorizationConfig struct { diff --git a/pkg/op/device_test.go b/pkg/op/device_test.go index 5fd9c9b..a7b5c4e 100644 --- a/pkg/op/device_test.go +++ b/pkg/op/device_test.go @@ -13,12 +13,12 @@ import ( "testing" "time" + "git.christmann.info/LARA/zitadel-oidc/v3/example/server/storage" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v3/example/server/storage" - "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/op" ) func Test_deviceAuthorizationHandler(t *testing.T) { diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 7aa7cf7..9b3ddb6 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -6,8 +6,8 @@ import ( jose "github.com/go-jose/go-jose/v4" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) type DiscoverStorage interface { diff --git a/pkg/op/discovery_test.go b/pkg/op/discovery_test.go index 61afb62..63f1b98 100644 --- a/pkg/op/discovery_test.go +++ b/pkg/op/discovery_test.go @@ -11,9 +11,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/op" - "github.com/zitadel/oidc/v3/pkg/op/mock" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op/mock" ) func TestDiscover(t *testing.T) { diff --git a/pkg/op/endpoint_test.go b/pkg/op/endpoint_test.go index bf112ef..5b98c6e 100644 --- a/pkg/op/endpoint_test.go +++ b/pkg/op/endpoint_test.go @@ -3,8 +3,8 @@ package op_test import ( "testing" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v3/pkg/op" ) func TestEndpoint_Path(t *testing.T) { diff --git a/pkg/op/error.go b/pkg/op/error.go index d57da83..272f85e 100644 --- a/pkg/op/error.go +++ b/pkg/op/error.go @@ -7,8 +7,8 @@ import ( "log/slog" "net/http" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) type ErrAuthRequest interface { diff --git a/pkg/op/error_test.go b/pkg/op/error_test.go index 107f9d0..9271cf1 100644 --- a/pkg/op/error_test.go +++ b/pkg/op/error_test.go @@ -11,9 +11,9 @@ import ( "strings" "testing" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/schema" ) diff --git a/pkg/op/keys.go b/pkg/op/keys.go index c96c456..97e400b 100644 --- a/pkg/op/keys.go +++ b/pkg/op/keys.go @@ -6,7 +6,7 @@ import ( jose "github.com/go-jose/go-jose/v4" - httphelper "github.com/zitadel/oidc/v3/pkg/http" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" ) type KeyProvider interface { diff --git a/pkg/op/keys_test.go b/pkg/op/keys_test.go index 3662739..9c80878 100644 --- a/pkg/op/keys_test.go +++ b/pkg/op/keys_test.go @@ -11,9 +11,9 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" - "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/op" - "github.com/zitadel/oidc/v3/pkg/op/mock" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op/mock" ) func TestKeys(t *testing.T) { diff --git a/pkg/op/mock/authorizer.mock.go b/pkg/op/mock/authorizer.mock.go index c7703f1..56b28e0 100644 --- a/pkg/op/mock/authorizer.mock.go +++ b/pkg/op/mock/authorizer.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/v3/pkg/op (interfaces: Authorizer) +// Source: git.christmann.info/LARA/zitadel-oidc/v3/pkg/op (interfaces: Authorizer) // Package mock is a generated GoMock package. package mock @@ -9,9 +9,9 @@ import ( slog "log/slog" reflect "reflect" + http "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + op "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" gomock "github.com/golang/mock/gomock" - http "github.com/zitadel/oidc/v3/pkg/http" - op "github.com/zitadel/oidc/v3/pkg/op" ) // MockAuthorizer is a mock of Authorizer interface. diff --git a/pkg/op/mock/authorizer.mock.impl.go b/pkg/op/mock/authorizer.mock.impl.go index 59e8fa3..73c4154 100644 --- a/pkg/op/mock/authorizer.mock.impl.go +++ b/pkg/op/mock/authorizer.mock.impl.go @@ -8,8 +8,8 @@ import ( "github.com/golang/mock/gomock" "github.com/zitadel/schema" - "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/op" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" ) func NewAuthorizer(t *testing.T) op.Authorizer { diff --git a/pkg/op/mock/client.go b/pkg/op/mock/client.go index f01e3ec..e2a5e85 100644 --- a/pkg/op/mock/client.go +++ b/pkg/op/mock/client.go @@ -5,8 +5,8 @@ import ( "github.com/golang/mock/gomock" - "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/op" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" ) func NewClient(t *testing.T) op.Client { diff --git a/pkg/op/mock/client.mock.go b/pkg/op/mock/client.mock.go index 9be0807..93eca67 100644 --- a/pkg/op/mock/client.mock.go +++ b/pkg/op/mock/client.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/v3/pkg/op (interfaces: Client) +// Source: git.christmann.info/LARA/zitadel-oidc/v3/pkg/op (interfaces: Client) // Package mock is a generated GoMock package. package mock @@ -8,9 +8,9 @@ import ( reflect "reflect" time "time" + oidc "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + op "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" gomock "github.com/golang/mock/gomock" - oidc "github.com/zitadel/oidc/v3/pkg/oidc" - op "github.com/zitadel/oidc/v3/pkg/op" ) // MockClient is a mock of Client interface. diff --git a/pkg/op/mock/configuration.mock.go b/pkg/op/mock/configuration.mock.go index 0ef9d92..bf51035 100644 --- a/pkg/op/mock/configuration.mock.go +++ b/pkg/op/mock/configuration.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/v3/pkg/op (interfaces: Configuration) +// Source: git.christmann.info/LARA/zitadel-oidc/v3/pkg/op (interfaces: Configuration) // Package mock is a generated GoMock package. package mock @@ -8,8 +8,8 @@ import ( http "net/http" reflect "reflect" + op "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" gomock "github.com/golang/mock/gomock" - op "github.com/zitadel/oidc/v3/pkg/op" language "golang.org/x/text/language" ) diff --git a/pkg/op/mock/discovery.mock.go b/pkg/op/mock/discovery.mock.go index a27f8ef..c85f91b 100644 --- a/pkg/op/mock/discovery.mock.go +++ b/pkg/op/mock/discovery.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/v3/pkg/op (interfaces: DiscoverStorage) +// Source: git.christmann.info/LARA/zitadel-oidc/v3/pkg/op (interfaces: DiscoverStorage) // Package mock is a generated GoMock package. package mock diff --git a/pkg/op/mock/generate.go b/pkg/op/mock/generate.go index e5cab3e..3d58ab7 100644 --- a/pkg/op/mock/generate.go +++ b/pkg/op/mock/generate.go @@ -1,11 +1,11 @@ package mock //go:generate go install github.com/golang/mock/mockgen@v1.6.0 -//go:generate mockgen -package mock -destination ./storage.mock.go github.com/zitadel/oidc/v3/pkg/op Storage -//go:generate mockgen -package mock -destination ./authorizer.mock.go github.com/zitadel/oidc/v3/pkg/op Authorizer -//go:generate mockgen -package mock -destination ./client.mock.go github.com/zitadel/oidc/v3/pkg/op Client -//go:generate mockgen -package mock -destination ./glob.mock.go github.com/zitadel/oidc/v3/pkg/op HasRedirectGlobs -//go:generate mockgen -package mock -destination ./configuration.mock.go github.com/zitadel/oidc/v3/pkg/op Configuration -//go:generate mockgen -package mock -destination ./discovery.mock.go github.com/zitadel/oidc/v3/pkg/op DiscoverStorage -//go:generate mockgen -package mock -destination ./signer.mock.go github.com/zitadel/oidc/v3/pkg/op SigningKey,Key -//go:generate mockgen -package mock -destination ./key.mock.go github.com/zitadel/oidc/v3/pkg/op KeyProvider +//go:generate mockgen -package mock -destination ./storage.mock.go git.christmann.info/LARA/zitadel-oidc/v3/pkg/op Storage +//go:generate mockgen -package mock -destination ./authorizer.mock.go git.christmann.info/LARA/zitadel-oidc/v3/pkg/op Authorizer +//go:generate mockgen -package mock -destination ./client.mock.go git.christmann.info/LARA/zitadel-oidc/v3/pkg/op Client +//go:generate mockgen -package mock -destination ./glob.mock.go git.christmann.info/LARA/zitadel-oidc/v3/pkg/op HasRedirectGlobs +//go:generate mockgen -package mock -destination ./configuration.mock.go git.christmann.info/LARA/zitadel-oidc/v3/pkg/op Configuration +//go:generate mockgen -package mock -destination ./discovery.mock.go git.christmann.info/LARA/zitadel-oidc/v3/pkg/op DiscoverStorage +//go:generate mockgen -package mock -destination ./signer.mock.go git.christmann.info/LARA/zitadel-oidc/v3/pkg/op SigningKey,Key +//go:generate mockgen -package mock -destination ./key.mock.go git.christmann.info/LARA/zitadel-oidc/v3/pkg/op KeyProvider diff --git a/pkg/op/mock/glob.go b/pkg/op/mock/glob.go index cade476..8149c8f 100644 --- a/pkg/op/mock/glob.go +++ b/pkg/op/mock/glob.go @@ -3,9 +3,9 @@ package mock import ( "testing" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + op "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" gomock "github.com/golang/mock/gomock" - "github.com/zitadel/oidc/v3/pkg/oidc" - op "github.com/zitadel/oidc/v3/pkg/op" ) func NewHasRedirectGlobs(t *testing.T) op.HasRedirectGlobs { diff --git a/pkg/op/mock/glob.mock.go b/pkg/op/mock/glob.mock.go index cf9996e..ebdc333 100644 --- a/pkg/op/mock/glob.mock.go +++ b/pkg/op/mock/glob.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/v3/pkg/op (interfaces: HasRedirectGlobs) +// Source: git.christmann.info/LARA/zitadel-oidc/v3/pkg/op (interfaces: HasRedirectGlobs) // Package mock is a generated GoMock package. package mock @@ -8,9 +8,9 @@ import ( reflect "reflect" time "time" + oidc "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + op "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" gomock "github.com/golang/mock/gomock" - oidc "github.com/zitadel/oidc/v3/pkg/oidc" - op "github.com/zitadel/oidc/v3/pkg/op" ) // MockHasRedirectGlobs is a mock of HasRedirectGlobs interface. diff --git a/pkg/op/mock/key.mock.go b/pkg/op/mock/key.mock.go index 122e852..d9ee857 100644 --- a/pkg/op/mock/key.mock.go +++ b/pkg/op/mock/key.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/v3/pkg/op (interfaces: KeyProvider) +// Source: git.christmann.info/LARA/zitadel-oidc/v3/pkg/op (interfaces: KeyProvider) // Package mock is a generated GoMock package. package mock @@ -8,8 +8,8 @@ import ( context "context" reflect "reflect" + op "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" gomock "github.com/golang/mock/gomock" - op "github.com/zitadel/oidc/v3/pkg/op" ) // MockKeyProvider is a mock of KeyProvider interface. diff --git a/pkg/op/mock/signer.mock.go b/pkg/op/mock/signer.mock.go index e1bab91..751ce60 100644 --- a/pkg/op/mock/signer.mock.go +++ b/pkg/op/mock/signer.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/v3/pkg/op (interfaces: SigningKey,Key) +// Source: git.christmann.info/LARA/zitadel-oidc/v3/pkg/op (interfaces: SigningKey,Key) // Package mock is a generated GoMock package. package mock diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index 02a7c5c..0df9830 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/zitadel/oidc/v3/pkg/op (interfaces: Storage) +// Source: git.christmann.info/LARA/zitadel-oidc/v3/pkg/op (interfaces: Storage) // Package mock is a generated GoMock package. package mock @@ -9,10 +9,10 @@ import ( reflect "reflect" time "time" + oidc "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + op "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" jose "github.com/go-jose/go-jose/v4" gomock "github.com/golang/mock/gomock" - oidc "github.com/zitadel/oidc/v3/pkg/oidc" - op "github.com/zitadel/oidc/v3/pkg/op" ) // MockStorage is a mock of Storage interface. diff --git a/pkg/op/mock/storage.mock.impl.go b/pkg/op/mock/storage.mock.impl.go index 002da7e..96e08a9 100644 --- a/pkg/op/mock/storage.mock.impl.go +++ b/pkg/op/mock/storage.mock.impl.go @@ -8,8 +8,8 @@ import ( "github.com/golang/mock/gomock" - "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/op" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" ) func NewStorage(t *testing.T) op.Storage { diff --git a/pkg/op/op.go b/pkg/op/op.go index 58ae838..76c2c89 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -14,8 +14,8 @@ import ( "go.opentelemetry.io/otel" "golang.org/x/text/language" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) const ( diff --git a/pkg/op/op_test.go b/pkg/op/op_test.go index 9a4a624..e1ac0bd 100644 --- a/pkg/op/op_test.go +++ b/pkg/op/op_test.go @@ -11,12 +11,12 @@ import ( "testing" "time" + "git.christmann.info/LARA/zitadel-oidc/v3/example/server/storage" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v3/example/server/storage" - "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/op" "golang.org/x/text/language" ) diff --git a/pkg/op/probes.go b/pkg/op/probes.go index cb3853d..fa713da 100644 --- a/pkg/op/probes.go +++ b/pkg/op/probes.go @@ -5,7 +5,7 @@ import ( "errors" "net/http" - httphelper "github.com/zitadel/oidc/v3/pkg/http" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" ) type ProbesFn func(context.Context) error diff --git a/pkg/op/server.go b/pkg/op/server.go index b500e43..d45b734 100644 --- a/pkg/op/server.go +++ b/pkg/op/server.go @@ -5,9 +5,9 @@ import ( "net/http" "net/url" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/muhlemmer/gu" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" ) // Server describes the interface that needs to be implemented to serve diff --git a/pkg/op/server_http.go b/pkg/op/server_http.go index 725dd64..d71a354 100644 --- a/pkg/op/server_http.go +++ b/pkg/op/server_http.go @@ -6,11 +6,11 @@ import ( "net/http" "net/url" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/go-chi/chi/v5" "github.com/rs/cors" "github.com/zitadel/logging" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/schema" ) diff --git a/pkg/op/server_http_routes_test.go b/pkg/op/server_http_routes_test.go index 1bfb32b..02200ee 100644 --- a/pkg/op/server_http_routes_test.go +++ b/pkg/op/server_http_routes_test.go @@ -14,9 +14,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v3/pkg/client" - "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/op" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" ) func jwtProfile() (string, error) { diff --git a/pkg/op/server_http_test.go b/pkg/op/server_http_test.go index 9ff07bc..75d02ca 100644 --- a/pkg/op/server_http_test.go +++ b/pkg/op/server_http_test.go @@ -14,11 +14,11 @@ import ( "testing" "time" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/schema" ) diff --git a/pkg/op/server_legacy.go b/pkg/op/server_legacy.go index 126fde1..06e4e93 100644 --- a/pkg/op/server_legacy.go +++ b/pkg/op/server_legacy.go @@ -6,8 +6,8 @@ import ( "net/http" "time" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/go-chi/chi/v5" - "github.com/zitadel/oidc/v3/pkg/oidc" ) // ExtendedLegacyServer allows embedding [LegacyServer] in a struct, diff --git a/pkg/op/session.go b/pkg/op/session.go index eb67b3c..ac663c9 100644 --- a/pkg/op/session.go +++ b/pkg/op/session.go @@ -8,8 +8,8 @@ import ( "net/url" "path" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) type SessionEnder interface { diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 35d7040..2dbd124 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -8,7 +8,7 @@ import ( jose "github.com/go-jose/go-jose/v4" "golang.org/x/text/language" - "github.com/zitadel/oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) type AuthStorage interface { diff --git a/pkg/op/token.go b/pkg/op/token.go index 1df9cc2..2e25d05 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -5,8 +5,8 @@ import ( "slices" "time" - "github.com/zitadel/oidc/v3/pkg/crypto" - "github.com/zitadel/oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) type TokenCreator interface { diff --git a/pkg/op/token_client_credentials.go b/pkg/op/token_client_credentials.go index 63dcc79..ddb2fbf 100644 --- a/pkg/op/token_client_credentials.go +++ b/pkg/op/token_client_credentials.go @@ -5,8 +5,8 @@ import ( "net/http" "net/url" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) // ClientCredentialsExchange handles the OAuth 2.0 client_credentials grant, including diff --git a/pkg/op/token_code.go b/pkg/op/token_code.go index 3612240..155aa43 100644 --- a/pkg/op/token_code.go +++ b/pkg/op/token_code.go @@ -4,8 +4,8 @@ import ( "context" "net/http" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) // CodeExchange handles the OAuth 2.0 authorization_code grant, including diff --git a/pkg/op/token_exchange.go b/pkg/op/token_exchange.go index fcb4468..00af485 100644 --- a/pkg/op/token_exchange.go +++ b/pkg/op/token_exchange.go @@ -7,8 +7,8 @@ import ( "strings" "time" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) type TokenExchangeRequest interface { diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go index 29234e1..bb6a5a0 100644 --- a/pkg/op/token_intospection.go +++ b/pkg/op/token_intospection.go @@ -5,8 +5,8 @@ import ( "errors" "net/http" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) type Introspector interface { diff --git a/pkg/op/token_jwt_profile.go b/pkg/op/token_jwt_profile.go index d1a7ff5..defb937 100644 --- a/pkg/op/token_jwt_profile.go +++ b/pkg/op/token_jwt_profile.go @@ -5,8 +5,8 @@ import ( "net/http" "time" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) type JWTAuthorizationGrantExchanger interface { diff --git a/pkg/op/token_refresh.go b/pkg/op/token_refresh.go index 7c8c1c0..a87e883 100644 --- a/pkg/op/token_refresh.go +++ b/pkg/op/token_refresh.go @@ -7,8 +7,8 @@ import ( "slices" "time" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) type RefreshTokenRequest interface { diff --git a/pkg/op/token_request.go b/pkg/op/token_request.go index 66e4c83..3f5af7a 100644 --- a/pkg/op/token_request.go +++ b/pkg/op/token_request.go @@ -6,8 +6,8 @@ import ( "net/http" "net/url" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) type Exchanger interface { diff --git a/pkg/op/token_request_test.go b/pkg/op/token_request_test.go index 21cf20b..d226af6 100644 --- a/pkg/op/token_request_test.go +++ b/pkg/op/token_request_test.go @@ -3,22 +3,22 @@ package op_test import ( "testing" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" "github.com/stretchr/testify/assert" - "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/op" ) func TestAuthorizeCodeChallenge(t *testing.T) { tests := []struct { - name string - codeVerifier string - codeChallenge *oidc.CodeChallenge - want func(t *testing.T, err error) + name string + codeVerifier string + codeChallenge *oidc.CodeChallenge + want func(t *testing.T, err error) }{ { - name: "missing both code_verifier and code_challenge", - codeVerifier: "", - codeChallenge: nil, + name: "missing both code_verifier and code_challenge", + codeVerifier: "", + codeChallenge: nil, want: func(t *testing.T, err error) { assert.Nil(t, err) }, @@ -46,9 +46,9 @@ func TestAuthorizeCodeChallenge(t *testing.T) { }, }, { - name: "code_verifier provided without code_challenge", - codeVerifier: "code_verifier", - codeChallenge: nil, + name: "code_verifier provided without code_challenge", + codeVerifier: "code_verifier", + codeChallenge: nil, want: func(t *testing.T, err error) { assert.ErrorContains(t, err, "code_verifier unexpectedly provided") }, diff --git a/pkg/op/token_revocation.go b/pkg/op/token_revocation.go index a86a481..049ee15 100644 --- a/pkg/op/token_revocation.go +++ b/pkg/op/token_revocation.go @@ -7,8 +7,8 @@ import ( "net/url" "strings" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) type Revoker interface { diff --git a/pkg/op/userinfo.go b/pkg/op/userinfo.go index 839b139..ff75e72 100644 --- a/pkg/op/userinfo.go +++ b/pkg/op/userinfo.go @@ -6,8 +6,8 @@ import ( "net/http" "strings" - httphelper "github.com/zitadel/oidc/v3/pkg/http" - "github.com/zitadel/oidc/v3/pkg/oidc" + httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) type UserinfoProvider interface { diff --git a/pkg/op/verifier_access_token.go b/pkg/op/verifier_access_token.go index 6ac29f2..585ca54 100644 --- a/pkg/op/verifier_access_token.go +++ b/pkg/op/verifier_access_token.go @@ -3,7 +3,7 @@ package op import ( "context" - "github.com/zitadel/oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) type AccessTokenVerifier oidc.Verifier diff --git a/pkg/op/verifier_access_token_example_test.go b/pkg/op/verifier_access_token_example_test.go index 397a2d3..b97a7fd 100644 --- a/pkg/op/verifier_access_token_example_test.go +++ b/pkg/op/verifier_access_token_example_test.go @@ -4,9 +4,9 @@ import ( "context" "fmt" - tu "github.com/zitadel/oidc/v3/internal/testutil" - "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/op" + tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" ) // MyCustomClaims extends the TokenClaims base, diff --git a/pkg/op/verifier_access_token_test.go b/pkg/op/verifier_access_token_test.go index 66e32ce..5845f9f 100644 --- a/pkg/op/verifier_access_token_test.go +++ b/pkg/op/verifier_access_token_test.go @@ -5,10 +5,10 @@ import ( "testing" "time" + tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - tu "github.com/zitadel/oidc/v3/internal/testutil" - "github.com/zitadel/oidc/v3/pkg/oidc" ) func TestNewAccessTokenVerifier(t *testing.T) { diff --git a/pkg/op/verifier_id_token_hint.go b/pkg/op/verifier_id_token_hint.go index 331c64c..02610aa 100644 --- a/pkg/op/verifier_id_token_hint.go +++ b/pkg/op/verifier_id_token_hint.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/zitadel/oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) type IDTokenHintVerifier oidc.Verifier diff --git a/pkg/op/verifier_id_token_hint_test.go b/pkg/op/verifier_id_token_hint_test.go index 597e291..347e33c 100644 --- a/pkg/op/verifier_id_token_hint_test.go +++ b/pkg/op/verifier_id_token_hint_test.go @@ -6,10 +6,10 @@ import ( "testing" "time" + tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - tu "github.com/zitadel/oidc/v3/internal/testutil" - "github.com/zitadel/oidc/v3/pkg/oidc" ) func TestNewIDTokenHintVerifier(t *testing.T) { diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index 06a7d34..85bfb14 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -8,7 +8,7 @@ import ( jose "github.com/go-jose/go-jose/v4" - "github.com/zitadel/oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" ) // JWTProfileVerfiier extends oidc.Verifier with diff --git a/pkg/op/verifier_jwt_profile_test.go b/pkg/op/verifier_jwt_profile_test.go index d96cbb4..2068678 100644 --- a/pkg/op/verifier_jwt_profile_test.go +++ b/pkg/op/verifier_jwt_profile_test.go @@ -5,11 +5,11 @@ import ( "testing" "time" + tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" + "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - tu "github.com/zitadel/oidc/v3/internal/testutil" - "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/op" ) func TestNewJWTProfileVerifier(t *testing.T) {