Repository: zitadel/oidc
Pull request #508 (2024-01-03 22:21:30)

fix: Implement package-level error for RevokeToken
By using package-level errors, as library consumer I'm able to catch specific error without do a string compare of the error messages.

After the PR, I can use `errors.Is(err, rp.ErrRelyingPartyNotSupportRevokeCaller)` to catch the specific error.

See: https://go.dev/blog/go1.13-errors

### Definition of Ready

- [x] I am happy with the code
- [x] 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.
- [x] No debug or dead code
- [x] My code has no repetitions
- [ ] Critical parts are tested automatically
- [ ] Where possible E2E tests are implemented
- [ ] Documentation/examples are up-to-date
- [ ] All non-functional requirements are met
- [x] Functionality of the acceptance criteria is checked manually on the dev system.



codecov[bot]: ## [Codecov](https://app.codecov.io/gh/zitadel/oidc/pull/508?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel) Report
Attention: `1 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`2b35eeb`)](https://app.codecov.io/gh/zitadel/oidc/commit/2b35eeb83560d69c6b492eb5dcad02ad30c6da55?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel) 60.60% compared to head [(`98fdb39`)](https://app.codecov.io/gh/zitadel/oidc/pull/508?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel) 60.65%.
> Report is 2 commits behind head on main.

| [Files](https://app.codecov.io/gh/zitadel/oidc/pull/508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel) | Patch % | Lines |
|---|---|---|
| [pkg/client/rp/relying\_party.go](https://app.codecov.io/gh/zitadel/oidc/pull/508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel#diff-cGtnL2NsaWVudC9ycC9yZWx5aW5nX3BhcnR5Lmdv) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/zitadel/oidc/pull/508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #508      +/-   ##
==========================================
+ Coverage   60.60%   60.65%   +0.05%     
==========================================
  Files          78       78              
  Lines        6792     6801       +9     
==========================================
+ Hits         4116     4125       +9     
  Misses       2379     2379              
  Partials      297      297              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/zitadel/oidc/pull/508?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel).


Patch:
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/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
 }
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/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))
+}


Test patch:
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) {