From 700d17d3b7d507de1b1d459a7261d6fb2571ebe3 Mon Sep 17 00:00:00 2001 From: Patrik Date: Thu, 2 Apr 2020 11:35:32 +0200 Subject: [PATCH] Merge pull request from GHSA-3p3g-vpw6-4w66 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: This patch requires a new SQL Table which needs to be created using `hydra migrate sql`. No other breaking changes have been introduced by this patch. This patch introduces a blacklist for JTIs which prevents a potential replay of `private_key_jwt` JWTs when performing client authorization. ## GHSA-3p3g-vpw6-4w66 ### Impact When using client authentication method "private_key_jwt" [1], OpenId specification says the following about assertion `jti`: > A unique identifier for the token, which can be used to prevent reuse of the token. These tokens MUST only be used once, unless conditions for reuse were negotiated between the parties Hydra does not seem to check the uniqueness of this `jti` value. Here is me sending the same token request twice, hence with the same `jti` assertion, and getting two access tokens: ``` $ curl --insecure --location --request POST 'https://localhost/_/oauth2/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode 'client_id=c001d00d-5ecc-beef-ca4e-b00b1e54a111' \ --data-urlencode 'scope=application openid' \ --data-urlencode 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer' \ --data-urlencode 'client_assertion=eyJhb [...] jTw' {"access_token":"zeG0NoqOtlACl8q5J6A-TIsNegQRRUzqLZaYrQtoBZQ.VR6iUcJQYp3u_j7pwvL7YtPqGhtyQe5OhnBE2KCp5pM","expires_in":3599,"scope":"application openid","token_type":"bearer"}⏎ ~$ curl --insecure --location --request POST 'https://localhost/_/oauth2/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode 'client_id=c001d00d-5ecc-beef-ca4e-b00b1e54a111' \ --data-urlencode 'scope=application openid' \ --data-urlencode 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer' \ --data-urlencode 'client_assertion=eyJhb [...] jTw' {"access_token":"wOYtgCLxLXlELORrwZlmeiqqMQ4kRzV-STU2_Sollas.mwlQGCZWXN7G2IoegUe1P0Vw5iGoKrkOzOaplhMSjm4","expires_in":3599,"scope":"application openid","token_type":"bearer"} ``` ### Severity We rate the severity as medium because the following reasons make it hard to replay tokens without the patch: - TLS protects against MITM which makes it difficult to intercept valid tokens for replay attacks - The expiry time of the JWT gives only a short window of opportunity where it could be replayed ### Patches This will be patched with v1.4.0+oryOS.17 ### Workarounds Two workarounds have been identified: - Do not allow clients to use `private_key_jwt` - Use short expiry times for the JWTs ### References https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication ### Upstream This issue will be resolved in the upstream repository https://github.com/ory/fosite --- client/manager.go | 2 +- driver/configuration/provider_viper_test.go | 7 +- go.mod | 6 +- go.sum | 9 ++ oauth2/fosite_store_helpers.go | 116 ++++++++++++++++++++ oauth2/fosite_store_memory.go | 69 ++++++++++-- oauth2/fosite_store_sql.go | 70 ++++++++++-- oauth2/handler.go | 12 +- oauth2/migrations/sql/cockroach/10.sql | 6 + oauth2/migrations/sql/cockroach/11.sql | 10 ++ oauth2/migrations/sql/shared/11.sql | 11 ++ oauth2/migrations/sql/tests/11_test.sql | 59 ++++++++++ oauth2/oauth2_auth_code_test.go | 11 +- oauth2/oauth2_refresh_token_test.go | 7 +- oauth2/sql_migration_files.go | 104 +++++++++++++++++- oauth2/x_fosite_migrations_test.go | 11 +- x/clean_sql.go | 1 + 17 files changed, 464 insertions(+), 47 deletions(-) create mode 100644 oauth2/migrations/sql/cockroach/10.sql create mode 100644 oauth2/migrations/sql/cockroach/11.sql create mode 100644 oauth2/migrations/sql/shared/11.sql create mode 100644 oauth2/migrations/sql/tests/11_test.sql diff --git a/client/manager.go b/client/manager.go index 0c4ff7d885d..727592b86c9 100644 --- a/client/manager.go +++ b/client/manager.go @@ -35,7 +35,7 @@ type Manager interface { } type Storage interface { - fosite.Storage + GetClient(ctx context.Context, id string) (fosite.Client, error) CreateClient(ctx context.Context, c *Client) error diff --git a/driver/configuration/provider_viper_test.go b/driver/configuration/provider_viper_test.go index 444eaff5480..dab2853ff1f 100644 --- a/driver/configuration/provider_viper_test.go +++ b/driver/configuration/provider_viper_test.go @@ -8,13 +8,14 @@ import ( "strings" "testing" - "github.com/ory/hydra/x" - "github.com/ory/viper" - "github.com/ory/x/logrusx" "github.com/rs/cors" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/ory/hydra/x" + "github.com/ory/viper" + "github.com/ory/x/logrusx" ) func setupEnv(env map[string]string) func(t *testing.T) (func(), func()) { diff --git a/go.mod b/go.mod index d1be339dfe1..6815e91a64e 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/go-swagger/go-swagger v0.22.1-0.20200306221957-4aad3a5f78b8 github.com/gobuffalo/packr v1.24.0 github.com/gobwas/glob v0.2.3 - github.com/golang/mock v1.3.1 + github.com/golang/mock v1.4.3 github.com/google/uuid v1.1.1 github.com/gorilla/sessions v1.1.4-0.20181208214519-12bd4761fc66 github.com/gtank/cryptopasta v0.0.0-20170601214702-1f550f6f2f69 @@ -26,7 +26,7 @@ require ( github.com/oleiade/reflections v1.0.0 github.com/olekukonko/tablewriter v0.0.1 github.com/opentracing/opentracing-go v1.1.1-0.20190913142402-a7454ce5950e - github.com/ory/fosite v0.30.6 + github.com/ory/fosite v0.31.0 github.com/ory/go-acc v0.2.1 github.com/ory/graceful v0.1.1 github.com/ory/herodot v0.7.0 @@ -53,7 +53,7 @@ require ( github.com/uber/jaeger-client-go v2.22.1+incompatible github.com/urfave/negroni v1.0.0 go.opentelemetry.io/otel v0.2.1 - golang.org/x/crypto v0.0.0-20200320181102-891825fb96df + golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/tools v0.0.0-20200313205530-4303120df7d8 diff --git a/go.sum b/go.sum index 7687427674d..c4cd141f242 100644 --- a/go.sum +++ b/go.sum @@ -452,6 +452,8 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 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= @@ -737,6 +739,8 @@ github.com/ory/dockertest/v3 v3.5.4/go.mod h1:J8ZUbNB2FOhm1cFZW9xBpDsODqsSWcyYgt github.com/ory/fosite v0.29.0/go.mod h1:0atSZmXO7CAcs6NPMI/Qtot8tmZYj04Nddoold4S2h0= github.com/ory/fosite v0.30.6 h1:t1EQHkGv3gVODC9oBvoEi3nIyZ4kvC/ayCsvyswDvis= github.com/ory/fosite v0.30.6/go.mod h1:Lq9qQ9Sl6mcea2Tt8J7PU+wUeFYPZ+vg7N3zPVKGbN8= +github.com/ory/fosite v0.31.0 h1:NZ0FA4ywPEYrCGLNVBAz2dq8vTacLDbbO4Iiy68WCKQ= +github.com/ory/fosite v0.31.0/go.mod h1:lSSqjo8Kr/U1P3kJWxsNGHmq7TnH/7pS1ijvQRT7G+g= github.com/ory/go-acc v0.0.0-20181118080137-ddc355013f90 h1:Bpk3eqc3rbJT2mE+uS9ETzmi2cEL4RuIKz2iUeteh04= github.com/ory/go-acc v0.0.0-20181118080137-ddc355013f90/go.mod h1:sxnvPCxChFuSmTJGj8FdMupeq1BezCiEpDjTUXQ4hf4= github.com/ory/go-acc v0.2.1 h1:Pwcmwd/cSnwJsYN76+w3HU7oXeWFTkwj/KUj1qGDrVw= @@ -1025,6 +1029,8 @@ golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAak golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200320181102-891825fb96df h1:lDWgvUvNnaTnNBc/dwOty86cFeKoKWbwy2wQj0gIxbU= golang.org/x/crypto v0.0.0-20200320181102-891825fb96df/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1153,6 +1159,7 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepx golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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= @@ -1324,5 +1331,7 @@ mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f/go.mod h1:4G1h5nDURzA3bwVMZI mvdan.cc/unparam v0.0.0-20190917161559-b83a221c10a2/go.mod h1:rCqoQrfAmpTX/h2APczwM7UymU/uvaOluiVPIYCSY/k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4= diff --git a/oauth2/fosite_store_helpers.go b/oauth2/fosite_store_helpers.go index 50601db2fdd..9e6a75c26cc 100644 --- a/oauth2/fosite_store_helpers.go +++ b/oauth2/fosite_store_helpers.go @@ -22,11 +22,14 @@ package oauth2 import ( "context" + "crypto/sha256" "fmt" "net/url" "testing" "time" + "github.com/ory/hydra/x" + "github.com/ory/fosite/storage" "github.com/ory/x/sqlxx" @@ -44,6 +47,33 @@ import ( "github.com/ory/hydra/consent" ) +func signatureFromJTI(jti string) string { + return fmt.Sprintf("%x", sha256.Sum256([]byte(jti))) +} + +type blacklistedJTI struct { + JTI string + Signature string `db:"signature"` + Expiry time.Time `db:"expires_at"` +} + +func newBlacklistedJTI(jti string, exp time.Time) *blacklistedJTI { + return &blacklistedJTI{ + JTI: jti, + Signature: signatureFromJTI(jti), + // because the database timestamp types are not as accurate as time.Time we truncate to seconds (which should always work) + Expiry: exp.UTC().Truncate(time.Second), + } +} + +type assertionJWTReader interface { + x.FositeStorer + + getClientAssertionJWT(ctx context.Context, jti string) (*blacklistedJTI, error) + + setClientAssertionJWT(context.Context, *blacklistedJTI) error +} + var defaultRequest = fosite.Request{ ID: "blank", RequestedAt: time.Now().UTC().Round(time.Second), @@ -135,6 +165,8 @@ func TestHelperRunner(t *testing.T, store InternalRegistry, k string) { t.Run(fmt.Sprintf("case=testHelperRevokeRefreshToken/db=%s", k), testHelperRevokeRefreshToken(store)) t.Run(fmt.Sprintf("case=testHelperCreateGetDeletePKCERequestSession/db=%s", k), testHelperCreateGetDeletePKCERequestSession(store)) t.Run(fmt.Sprintf("case=testHelperFlushTokens/db=%s", k), testHelperFlushTokens(store, time.Hour)) + t.Run(fmt.Sprintf("case=testFositeStoreSetClientAssertionJWT/db=%s", k), testFositeStoreSetClientAssertionJWT(store)) + t.Run(fmt.Sprintf("case=testFositeStoreClientAssertionJWTValid/db=%s", k), testFositeStoreClientAssertionJWTValid(store)) } func testHelperUniqueConstraints(m InternalRegistry, storageType string) func(t *testing.T) { @@ -531,6 +563,90 @@ func testFositeSqlStoreTransactionRollbackOpenIdConnectSession(m InternalRegistr } } +func testFositeStoreSetClientAssertionJWT(m InternalRegistry) func(*testing.T) { + return func(t *testing.T) { + t.Run("case=basic setting works", func(t *testing.T) { + store, ok := m.OAuth2Storage().(assertionJWTReader) + require.True(t, ok) + jti := newBlacklistedJTI("basic jti", time.Now().Add(time.Minute)) + + require.NoError(t, store.SetClientAssertionJWT(context.Background(), jti.JTI, jti.Expiry)) + + cmp, err := store.getClientAssertionJWT(context.Background(), jti.JTI) + require.NoError(t, err) + assert.Equal(t, jti, cmp) + }) + + t.Run("case=errors when the JTI is blacklisted", func(t *testing.T) { + store, ok := m.OAuth2Storage().(assertionJWTReader) + require.True(t, ok) + jti := newBlacklistedJTI("already set jti", time.Now().Add(time.Minute)) + require.NoError(t, store.setClientAssertionJWT(context.Background(), jti)) + + assert.True(t, errors.Is(store.SetClientAssertionJWT(context.Background(), jti.JTI, jti.Expiry), fosite.ErrJTIKnown)) + }) + + t.Run("case=deletes expired JTIs", func(t *testing.T) { + store, ok := m.OAuth2Storage().(assertionJWTReader) + require.True(t, ok) + expiredJTI := newBlacklistedJTI("expired jti", time.Now().Add(-time.Minute)) + require.NoError(t, store.setClientAssertionJWT(context.Background(), expiredJTI)) + newJTI := newBlacklistedJTI("some new jti", time.Now().Add(time.Minute)) + + require.NoError(t, store.SetClientAssertionJWT(context.Background(), newJTI.JTI, newJTI.Expiry)) + + _, err := store.getClientAssertionJWT(context.Background(), expiredJTI.JTI) + assert.True(t, errors.Is(err, sqlcon.ErrNoRows)) + cmp, err := store.getClientAssertionJWT(context.Background(), newJTI.JTI) + assert.Equal(t, newJTI, cmp) + }) + + t.Run("case=inserts same JTI if expired", func(t *testing.T) { + store, ok := m.OAuth2Storage().(assertionJWTReader) + require.True(t, ok) + jti := newBlacklistedJTI("going to be reused jti", time.Now().Add(-time.Minute)) + require.NoError(t, store.setClientAssertionJWT(context.Background(), jti)) + + jti.Expiry = jti.Expiry.Add(2 * time.Minute) + assert.NoError(t, store.SetClientAssertionJWT(context.Background(), jti.JTI, jti.Expiry)) + cmp, err := store.getClientAssertionJWT(context.Background(), jti.JTI) + assert.NoError(t, err) + assert.Equal(t, jti, cmp) + }) + } +} + +func testFositeStoreClientAssertionJWTValid(m InternalRegistry) func(*testing.T) { + return func(t *testing.T) { + t.Run("case=returns valid on unknown JTI", func(t *testing.T) { + store, ok := m.OAuth2Storage().(assertionJWTReader) + require.True(t, ok) + + assert.NoError(t, store.ClientAssertionJWTValid(context.Background(), "unknown jti")) + }) + + t.Run("case=returns invalid on known JTI", func(t *testing.T) { + store, ok := m.OAuth2Storage().(assertionJWTReader) + require.True(t, ok) + jti := newBlacklistedJTI("known jti", time.Now().Add(time.Minute)) + + require.NoError(t, store.setClientAssertionJWT(context.Background(), jti)) + + assert.True(t, errors.Is(store.ClientAssertionJWTValid(context.Background(), jti.JTI), fosite.ErrJTIKnown)) + }) + + t.Run("case=returns valid on expired JTI", func(t *testing.T) { + store, ok := m.OAuth2Storage().(assertionJWTReader) + require.True(t, ok) + jti := newBlacklistedJTI("expired jti", time.Now().Add(-time.Minute)) + + require.NoError(t, store.setClientAssertionJWT(context.Background(), jti)) + + assert.NoError(t, store.ClientAssertionJWTValid(context.Background(), jti.JTI)) + }) + } +} + func doTestCommit(m InternalRegistry, t *testing.T, createFn func(context.Context, string, fosite.Requester) error, getFn func(context.Context, string, fosite.Session) (fosite.Requester, error), diff --git a/oauth2/fosite_store_memory.go b/oauth2/fosite_store_memory.go index 127e64adbc2..d546a8f3161 100644 --- a/oauth2/fosite_store_memory.go +++ b/oauth2/fosite_store_memory.go @@ -34,11 +34,12 @@ import ( ) type FositeMemoryStore struct { - AuthorizeCodes map[string]authorizeCode - IDSessions map[string]fosite.Requester - AccessTokens map[string]fosite.Requester - RefreshTokens map[string]fosite.Requester - PKCES map[string]fosite.Requester + AuthorizeCodes map[string]authorizeCode + IDSessions map[string]fosite.Requester + AccessTokens map[string]fosite.Requester + RefreshTokens map[string]fosite.Requester + PKCES map[string]fosite.Requester + BlacklistedJTIs map[string]time.Time c Configuration r InternalRegistry @@ -53,11 +54,12 @@ func NewFositeMemoryStore( c Configuration, ) *FositeMemoryStore { return &FositeMemoryStore{ - AuthorizeCodes: make(map[string]authorizeCode), - IDSessions: make(map[string]fosite.Requester), - AccessTokens: make(map[string]fosite.Requester), - PKCES: make(map[string]fosite.Requester), - RefreshTokens: make(map[string]fosite.Requester), + AuthorizeCodes: make(map[string]authorizeCode), + IDSessions: make(map[string]fosite.Requester), + AccessTokens: make(map[string]fosite.Requester), + PKCES: make(map[string]fosite.Requester), + RefreshTokens: make(map[string]fosite.Requester), + BlacklistedJTIs: make(map[string]time.Time), c: c, r: r, @@ -73,6 +75,53 @@ func (s *FositeMemoryStore) GetClient(ctx context.Context, id string) (fosite.Cl return s.r.ClientManager().GetClient(ctx, id) } +func (s *FositeMemoryStore) ClientAssertionJWTValid(_ context.Context, jti string) error { + s.RLock() + defer s.RUnlock() + if exp, exists := s.BlacklistedJTIs[jti]; exists && exp.After(time.Now()) { + return errors.WithStack(fosite.ErrJTIKnown) + } + + return nil +} + +func (s *FositeMemoryStore) SetClientAssertionJWT(_ context.Context, jti string, exp time.Time) error { + s.Lock() + defer s.Unlock() + + for j, e := range s.BlacklistedJTIs { + if e.Before(time.Now()) { + delete(s.BlacklistedJTIs, j) + } + } + + if _, exists := s.BlacklistedJTIs[jti]; exists { + return errors.WithStack(fosite.ErrJTIKnown) + } + + s.BlacklistedJTIs[jti] = exp + return nil +} + +func (s *FositeMemoryStore) getClientAssertionJWT(_ context.Context, jti string) (*blacklistedJTI, error) { + s.RLock() + defer s.RUnlock() + + if exp, exists := s.BlacklistedJTIs[jti]; exists { + return newBlacklistedJTI(jti, exp), nil + } + + return nil, errors.WithStack(sqlcon.ErrNoRows) +} + +func (s *FositeMemoryStore) setClientAssertionJWT(_ context.Context, jti *blacklistedJTI) error { + s.Lock() + defer s.Unlock() + + s.BlacklistedJTIs[jti.JTI] = jti.Expiry + return nil +} + func (s *FositeMemoryStore) Authenticate(ctx context.Context, id string, secret []byte) (*client.Client, error) { return s.r.ClientManager().Authenticate(ctx, id, secret) } diff --git a/oauth2/fosite_store_sql.go b/oauth2/fosite_store_sql.go index b58648ec3a4..7c81396b149 100644 --- a/oauth2/fosite_store_sql.go +++ b/oauth2/fosite_store_sql.go @@ -31,18 +31,20 @@ import ( "time" "github.com/jmoiron/sqlx" - "github.com/ory/herodot" "github.com/pkg/errors" migrate "github.com/rubenv/sql-migrate" "github.com/sirupsen/logrus" "github.com/tidwall/gjson" + "github.com/ory/herodot" + "github.com/ory/fosite" - "github.com/ory/hydra/client" - "github.com/ory/hydra/jwk" "github.com/ory/x/dbal" "github.com/ory/x/sqlcon" "github.com/ory/x/stringsx" + + "github.com/ory/hydra/client" + "github.com/ory/hydra/jwk" ) type FositeSQLStore struct { @@ -69,11 +71,12 @@ func NewFositeSQLStore(db *sqlx.DB, r InternalRegistry, c Configuration, kc *jwk type tableName string const ( - sqlTableOpenID tableName = "oidc" - sqlTableAccess tableName = "access" - sqlTableRefresh tableName = "refresh" - sqlTableCode tableName = "code" - sqlTablePKCE tableName = "pkce" + sqlTableOpenID tableName = "oidc" + sqlTableAccess tableName = "access" + sqlTableRefresh tableName = "refresh" + sqlTableCode tableName = "code" + sqlTablePKCE tableName = "pkce" + sqlTableBlacklistedJTI tableName = "jti_blacklist" ) var Migrations = map[string]*dbal.PackrMigrationSource{ @@ -228,6 +231,57 @@ func (s *FositeSQLStore) GetClient(ctx context.Context, id string) (fosite.Clien return s.r.ClientManager().GetClient(ctx, id) } +func (s *FositeSQLStore) ClientAssertionJWTValid(ctx context.Context, jti string) error { + d, err := s.getClientAssertionJWT(ctx, jti) + if errors.Is(err, sqlcon.ErrNoRows) { + // the jti is not known => valid + return nil + } else if err != nil { + return err + } + if d.Expiry.After(time.Now()) { + // the jti is not expired yet => invalid + return errors.WithStack(fosite.ErrJTIKnown) + } + // the jti is expired => valid + return nil +} + +func (s *FositeSQLStore) SetClientAssertionJWT(ctx context.Context, j string, exp time.Time) error { + db := s.db(ctx) + + // delete expired + if _, err := db.ExecContext(ctx, fmt.Sprintf("DELETE FROM hydra_oauth2_%s WHERE expires_at < now()", sqlTableBlacklistedJTI)); err != nil { + return sqlcon.HandleError(err) + } + + if err := s.setClientAssertionJWT(ctx, newBlacklistedJTI(j, exp)); errors.Is(err, sqlcon.ErrUniqueViolation) { + // found a jti + return errors.WithStack(fosite.ErrJTIKnown) + } else if err != nil { + return err + } + // setting worked without a problem + return nil +} + +func (s *FositeSQLStore) getClientAssertionJWT(ctx context.Context, j string) (*blacklistedJTI, error) { + sig := signatureFromJTI(j) + jti := blacklistedJTI{ + JTI: j, + } + db := s.db(ctx) + + return &jti, sqlcon.HandleError(db.GetContext(ctx, &jti, db.Rebind(fmt.Sprintf("SELECT * FROM hydra_oauth2_%s WHERE signature=?", sqlTableBlacklistedJTI)), sig)) +} + +func (s *FositeSQLStore) setClientAssertionJWT(ctx context.Context, jti *blacklistedJTI) error { + db := s.db(ctx) + _, err := db.ExecContext(ctx, db.Rebind(fmt.Sprintf("INSERT INTO hydra_oauth2_%s (signature, expires_at) VALUES (?, ?)", sqlTableBlacklistedJTI)), jti.Signature, jti.Expiry) + + return sqlcon.HandleError(err) +} + func (s *FositeSQLStore) Authenticate(ctx context.Context, id string, secret []byte) (*client.Client, error) { return s.r.ClientManager().Authenticate(ctx, id, secret) } diff --git a/oauth2/handler.go b/oauth2/handler.go index e711040ac68..3f1b7258262 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -659,12 +659,12 @@ func (h *Handler) AuthHandler(w http.ResponseWriter, r *http.Request, _ httprout authorizeRequest.SetID(session.Challenge) claims := &jwt.IDTokenClaims{ - Subject: session.ConsentRequest.SubjectIdentifier, - Issuer: strings.TrimRight(h.c.IssuerURL().String(), "/") + "/", - IssuedAt: time.Now().UTC(), - AuthTime: time.Time(session.AuthenticatedAt), - RequestedAt: session.RequestedAt, - Extra: session.Session.IDToken, + Subject: session.ConsentRequest.SubjectIdentifier, + Issuer: strings.TrimRight(h.c.IssuerURL().String(), "/") + "/", + IssuedAt: time.Now().UTC(), + AuthTime: time.Time(session.AuthenticatedAt), + RequestedAt: session.RequestedAt, + Extra: session.Session.IDToken, AuthenticationContextClassReference: session.ConsentRequest.ACR, // We do not need to pass the audience because it's included directly by ORY Fosite diff --git a/oauth2/migrations/sql/cockroach/10.sql b/oauth2/migrations/sql/cockroach/10.sql new file mode 100644 index 00000000000..7113588879e --- /dev/null +++ b/oauth2/migrations/sql/cockroach/10.sql @@ -0,0 +1,6 @@ +-- Empty because CockroachDB creates indices for foreign keys automatically: +-- https://www.cockroachlabs.com/docs/stable/foreign-key.html + +-- +migrate Up + +-- +migrate Down diff --git a/oauth2/migrations/sql/cockroach/11.sql b/oauth2/migrations/sql/cockroach/11.sql new file mode 100644 index 00000000000..7c34fe73b9c --- /dev/null +++ b/oauth2/migrations/sql/cockroach/11.sql @@ -0,0 +1,10 @@ +-- +migrate Up +CREATE TABLE IF NOT EXISTS hydra_oauth2_jti_blacklist ( + signature varchar(64) NOT NULL PRIMARY KEY, + expires_at timestamp NOT NULL DEFAULT now() +); + +CREATE INDEX ON hydra_oauth2_jti_blacklist ( expires_at ); + +-- +migrate Down +DROP TABLE hydra_oauth2_jti_blacklist; diff --git a/oauth2/migrations/sql/shared/11.sql b/oauth2/migrations/sql/shared/11.sql new file mode 100644 index 00000000000..68764d1689a --- /dev/null +++ b/oauth2/migrations/sql/shared/11.sql @@ -0,0 +1,11 @@ +-- +migrate Up +CREATE TABLE IF NOT EXISTS hydra_oauth2_jti_blacklist ( + signature varchar(64) NOT NULL PRIMARY KEY, + expires_at timestamp NOT NULL DEFAULT now() +); + +-- mysql requires the index to be named +CREATE INDEX hydra_oauth2_jti_blacklist_expiry ON hydra_oauth2_jti_blacklist ( expires_at ); + +-- +migrate Down +DROP TABLE hydra_oauth2_jti_blacklist; diff --git a/oauth2/migrations/sql/tests/11_test.sql b/oauth2/migrations/sql/tests/11_test.sql new file mode 100644 index 00000000000..bf88305ad40 --- /dev/null +++ b/oauth2/migrations/sql/tests/11_test.sql @@ -0,0 +1,59 @@ +-- +migrate Up +INSERT INTO hydra_client (id, allowed_cors_origins, client_name, client_secret, redirect_uris, grant_types, response_types, scope, owner, policy_uri, tos_uri, client_uri, logo_uri, contacts, client_secret_expires_at, sector_identifier_uri, jwks, jwks_uri, token_endpoint_auth_method, request_uris, request_object_signing_alg, userinfo_signed_response_alg, subject_type, audience, frontchannel_logout_uri, frontchannel_logout_session_required, post_logout_redirect_uris, backchannel_logout_uri, backchannel_logout_session_required, metadata) +VALUES + ('11-client', 'http://localhost|http://google', 'some-client', 'abcdef', 'http://localhost|http://google', 'authorize_code|implicit', 'token|id_token', 'foo|bar', 'aeneas', 'http://policy', 'http://tos', 'http://client', 'http://logo', 'aeneas|foo', 0, 'http://sector', '{"keys": []}', 'http://jwks', 'none', 'http://uri1|http://uri2', 'rs256', 'rs526', 'public', 'https://www.ory.sh/api', 'http://fc-logout/', true, 'http://redir1/|http://redir2/', 'http://bc-logout/', true, '{"foo":"bar"}'); + +INSERT INTO + hydra_oauth2_authentication_session (id, authenticated_at, subject) +VALUES + ('11-login-session-id', NOW(), '11-sub'); + +INSERT INTO + hydra_oauth2_authentication_request (challenge, verifier, client_id, subject, request_url, skip, requested_scope, csrf, authenticated_at, requested_at, oidc_context, login_session_id, requested_at_audience) +VALUES + ('11-challenge', '11-verifier', '11-client', '11-subject', '11-redirect', false, '11-scope', '11-csrf', NOW(), NOW(), '{}', '11-login-session-id', '11-aud'); + +INSERT INTO + hydra_oauth2_consent_request (challenge, verifier, client_id, subject, request_url, skip, requested_scope, csrf, authenticated_at, requested_at, oidc_context, forced_subject_identifier, login_session_id, login_challenge, requested_at_audience, acr, context) +VALUES + ('11-challenge', '11-verifier', '11-client', '11-subject', '11-redirect', false, '11-scope', '11-csrf', NOW(), NOW(), '{}', '11-forced-sub', '11-login-session-id', '11-challenge', '11-aud', '11-acr', '{}'); + +INSERT INTO + hydra_oauth2_consent_request_handled (challenge, granted_scope, remember, remember_for, error, requested_at, session_access_token, session_id_token, authenticated_at, was_used, granted_at_audience) +VALUES + ('11-challenge', '11-scope', true, 3600, '{}', NOW(), '{}', '{}', NOW(), false, '11-aud'); + +-- The previous block is just to get foreign keys working + +INSERT INTO + hydra_oauth2_access (signature, request_id, requested_at, client_id, scope, granted_scope, form_data, session_data, subject, active, requested_audience, granted_audience, challenge_id) +VALUES + ('11-sig', '11-request', NOW(), '11-client', '11-scope', '11-granted-scope', '', '{}', '11-subject', true, '11-challengeed-aud', '11-granted-aud', '11-challenge'); + +INSERT INTO + hydra_oauth2_refresh (signature, request_id, requested_at, client_id, scope, granted_scope, form_data, session_data, subject, active, requested_audience, granted_audience, challenge_id) +VALUES + ('11-sig', '11-request', NOW(), '11-client', '11-scope', '11-granted-scope', '', '{}', '11-subject', true, '11-challengeed-aud', '11-granted-aud', '11-challenge'); + +INSERT INTO + hydra_oauth2_code (signature, request_id, requested_at, client_id, scope, granted_scope, form_data, session_data, subject, active, requested_audience, granted_audience, challenge_id) +VALUES + ('11-sig', '11-request', NOW(), '11-client', '11-scope', '11-granted-scope', '', '{}', '11-subject', true, '11-challengeed-aud', '11-granted-aud', '11-challenge'); + +INSERT INTO + hydra_oauth2_oidc (signature, request_id, requested_at, client_id, scope, granted_scope, form_data, session_data, subject, active, requested_audience, granted_audience, challenge_id) +VALUES + ('11-sig', '11-request', NOW(), '11-client', '11-scope', '11-granted-scope', '', '{}', '11-subject', true, '11-challengeed-aud', '11-granted-aud', '11-challenge'); + +INSERT INTO + hydra_oauth2_pkce (signature, request_id, requested_at, client_id, scope, granted_scope, form_data, session_data, subject, active, requested_audience, granted_audience, challenge_id) +VALUES + ('11-sig', '11-request', NOW(), '11-client', '11-scope', '11-granted-scope', '', '{}', '11-subject', true, '11-challengeed-aud', '11-granted-aud', '11-challenge'); + + -- 11-sig + INSERT INTO + hydra_oauth2_jti_blacklist (signature, expires_at) + VALUES + ('c5b4a7dc4798eb3047523b0db7a899958d64f92a8b24ef38057539ef32763abc', '2038-01-19 01:00:00'); + +-- +migrate Down diff --git a/oauth2/oauth2_auth_code_test.go b/oauth2/oauth2_auth_code_test.go index cc0388bc752..fa72b29bf74 100644 --- a/oauth2/oauth2_auth_code_test.go +++ b/oauth2/oauth2_auth_code_test.go @@ -38,6 +38,12 @@ import ( djwt "github.com/dgrijalva/jwt-go" "github.com/jmoiron/sqlx" "github.com/julienschmidt/httprouter" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" + "github.com/ory/fosite" "github.com/ory/fosite/token/jwt" hc "github.com/ory/hydra/client" @@ -52,11 +58,6 @@ import ( "github.com/ory/x/pointerx" "github.com/ory/x/sqlcon/dockertest" "github.com/ory/x/urlx" - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/oauth2" - "golang.org/x/oauth2/clientcredentials" ) func newCookieJar() http.CookieJar { diff --git a/oauth2/oauth2_refresh_token_test.go b/oauth2/oauth2_refresh_token_test.go index 3ef4dc2899d..30daae2c131 100644 --- a/oauth2/oauth2_refresh_token_test.go +++ b/oauth2/oauth2_refresh_token_test.go @@ -11,6 +11,10 @@ import ( "time" "github.com/jmoiron/sqlx" + "github.com/pborman/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/ory/fosite" hc "github.com/ory/hydra/client" "github.com/ory/hydra/driver" @@ -19,9 +23,6 @@ import ( "github.com/ory/x/dbal" "github.com/ory/x/errorsx" "github.com/ory/x/sqlcon/dockertest" - "github.com/pborman/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) // TestCreateRefreshTokenSessionStress is a sanity test to verify the fix for https://github.com/ory/hydra/issues/1719 & diff --git a/oauth2/sql_migration_files.go b/oauth2/sql_migration_files.go index ebab627bc9c..f6d7e32a99f 100644 --- a/oauth2/sql_migration_files.go +++ b/oauth2/sql_migration_files.go @@ -1,5 +1,7 @@ // Code generated for package oauth2 by go-bindata DO NOT EDIT. (@generated) // sources: +// migrations/sql/cockroach/10.sql +// migrations/sql/cockroach/11.sql // migrations/sql/cockroach/9.sql // migrations/sql/mysql/.gitkeep // migrations/sql/mysql/10.sql @@ -14,12 +16,14 @@ // migrations/sql/postgres/7.sql // migrations/sql/postgres/9.sql // migrations/sql/shared/1.sql +// migrations/sql/shared/11.sql // migrations/sql/shared/2.sql // migrations/sql/shared/3.sql // migrations/sql/shared/4.sql // migrations/sql/shared/8.sql // migrations/sql/tests/.gitkeep // migrations/sql/tests/10_test.sql +// migrations/sql/tests/11_test.sql // migrations/sql/tests/1_test.sql // migrations/sql/tests/2_test.sql // migrations/sql/tests/3_test.sql @@ -105,6 +109,46 @@ func (fi bindataFileInfo) Sys() interface{} { return nil } +var _migrationsSqlCockroach10Sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x54\xcc\x31\x0e\x83\x30\x0c\x85\xe1\x9d\x53\x78\xaf\x42\x76\xc6\x96\x1e\xa1\x07\x30\xc6\x85\x88\x24\x8e\x62\xa3\x28\xb7\xaf\x90\xda\xa1\xc3\x1b\xde\xf0\x7f\xce\xc1\x33\x15\xeb\xb0\x30\xe1\xa9\x0c\x0f\xa1\xa3\x0a\xd2\x3e\xdf\x81\x2a\xa3\xb1\x42\xc8\x6b\x20\x56\x78\x4b\xbd\xc6\x61\xcb\x70\x70\x57\xc0\xd3\x24\xa1\x05\xc2\x18\xfb\x34\x38\x07\xbb\x59\xd1\xc9\xfb\xd6\xda\x48\x3f\x2a\xe2\xa2\x23\x49\xf2\xab\x90\x7a\x35\x5c\x22\xfb\x2f\xe4\x0e\xee\xe3\x6e\x29\x0e\x57\x7e\x4b\x61\xab\x68\x0c\xaf\xf2\xff\x67\x69\x79\xf8\x04\x00\x00\xff\xff\xb3\xf4\x91\x1c\xad\x00\x00\x00") + +func migrationsSqlCockroach10SqlBytes() ([]byte, error) { + return bindataRead( + _migrationsSqlCockroach10Sql, + "migrations/sql/cockroach/10.sql", + ) +} + +func migrationsSqlCockroach10Sql() (*asset, error) { + bytes, err := migrationsSqlCockroach10SqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "migrations/sql/cockroach/10.sql", size: 173, mode: os.FileMode(420), modTime: time.Unix(1585815362, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _migrationsSqlCockroach11Sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x7c\x8f\xbd\x4e\xc3\x30\x14\x46\xe7\xf8\x29\xbe\x31\x11\x74\x41\x88\xa5\x53\x20\xae\x14\x61\x9c\xca\x75\xa4\x76\xb2\x2e\xc5\x6a\x0c\xcd\x8f\xec\x5b\x0a\x6f\x8f\xa8\x40\x74\xa1\x77\xbe\xdf\xd1\x39\xb3\x19\xae\xfa\xb0\x8b\xc4\x1e\xed\x24\x1e\x8c\x2c\xad\x84\x2d\xef\x95\x44\xbd\x80\x6e\x2c\xe4\xba\x5e\xd9\x15\xba\xcf\x97\x48\x6e\xa4\x03\x77\x37\xee\x95\x83\x7b\xde\xd3\xf6\x6d\x1f\x12\x23\x17\x59\x0a\xbb\x81\xf8\x10\x3d\x4e\x97\xbd\x53\xdc\x76\x14\xf3\xbb\xdb\xe2\x04\xd1\xad\x52\x58\x9a\xfa\xa9\x34\x1b\x3c\xca\xcd\xb5\xc8\xfc\xc7\x14\xa2\x4f\x8e\x18\xc8\x38\xf4\x3e\x31\xf5\xd3\xdf\x77\x25\x17\x65\xab\x2c\x86\xf1\x98\x17\xa2\x98\x8b\x5f\xbb\x5a\x57\x72\x8d\x46\x5f\x54\xc2\x19\xfe\x7b\x7b\x1e\x5a\x8d\xc7\x41\x54\xa6\x59\xfe\x84\xfe\xcf\x99\x8b\xaf\x00\x00\x00\xff\xff\x52\x42\x55\x41\x21\x01\x00\x00") + +func migrationsSqlCockroach11SqlBytes() ([]byte, error) { + return bindataRead( + _migrationsSqlCockroach11Sql, + "migrations/sql/cockroach/11.sql", + ) +} + +func migrationsSqlCockroach11Sql() (*asset, error) { + bytes, err := migrationsSqlCockroach11SqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "migrations/sql/cockroach/11.sql", size: 289, mode: os.FileMode(420), modTime: time.Unix(1585817202, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + var _migrationsSqlCockroach9Sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x98\x41\x6f\xda\x30\x14\xc7\xcf\xf8\x53\xbc\x5b\x41\xa3\xd2\x54\xad\x27\x4e\x19\x79\x4c\xd1\xb2\xd0\x05\x47\x6a\x4f\x91\xeb\x3c\x48\x56\x48\x58\x6c\xda\xed\xdb\x4f\xa6\x04\x82\x42\x28\x9b\x80\xa1\x2d\x57\xbf\x7f\xe2\x9f\xed\xdf\x33\x11\xd7\xd7\xf0\x6e\x96\x4c\x72\xa1\x09\x82\x39\xeb\xfb\x68\x71\x04\x6e\x7d\x74\x11\x9c\x01\x78\x43\x0e\x78\xef\x8c\xf8\x08\xe2\x9f\x51\x2e\xc2\x4c\x2c\x74\x7c\x13\x0a\x29\x49\x29\x68\xb3\x96\x4a\x26\xa9\xd0\x8b\x9c\xe0\x59\xe4\x32\x16\x79\xfb\xe6\xf6\xb6\xb3\x7c\xd0\x0b\x5c\x17\xee\x7c\xe7\x8b\xe5\x3f\xc0\x67\x7c\xe8\xb2\x56\x4e\xdf\x17\xa4\x74\x98\x44\xeb\xf8\x87\xf7\x9b\xf4\x26\x41\x51\x28\x34\xe8\x64\x46\x4a\x8b\xd9\x7c\xf3\x3e\x1b\x07\x56\xe0\x72\x48\xb3\x97\x76\xa7\xcb\x5a\x72\x9a\x50\xba\xf5\xc2\xad\xf9\xbb\xac\xa5\x64\x36\x27\xd0\xf4\x43\x97\x47\x27\xb9\x48\xcd\x2c\xbb\xab\xe3\x2c\x9f\x85\x91\xd0\xa2\x52\x51\xa4\x54\x92\xa5\x35\xc5\xc5\xe3\x37\x92\xba\x66\x2b\x0a\xf4\xab\xab\x2e\x6b\x09\xa9\x93\x67\x82\xc7\x2c\x9b\x56\x13\xdc\x0f\x70\x7b\x2f\x16\x51\x42\xa9\x2c\x40\x2b\x6f\x2b\x56\xf3\x56\x4e\xc6\x62\x3a\xa5\x74\x42\x95\x13\x78\x5d\x40\xe0\x39\x5f\x03\x84\xf6\xe6\x9c\xcc\x1e\x3b\x9e\x8d\xf7\xeb\xc1\xe5\xd1\x94\x86\xd7\x27\x50\x1e\x2b\xcd\xd3\x61\x9d\xde\xc1\x62\xe5\x34\xce\x49\xc5\x8d\x59\xff\x95\x59\x47\x55\x48\x66\x11\x35\xfe\xfc\x6b\xfe\x9c\xc2\x94\x2c\x89\x64\x63\x4a\x63\xca\xdb\xa6\xcc\x9f\x64\x73\xa7\xfc\x96\x29\x7f\xae\x07\xc7\xfb\x5d\xb2\x55\xf4\xa8\xc9\x1d\x53\x0f\x66\xb9\x1c\xfd\x95\x1e\xbb\x3e\x80\x2d\xdb\x86\xfe\xd0\x1b\x71\xdf\x72\x3c\xbe\x2b\x12\xae\x27\x0a\xc7\x4f\x30\x18\xfa\xe8\x7c\xf2\x8c\x1f\x65\x04\xf0\x71\x80\x3e\x7a\x7d\x2c\xbc\x7b\xad\xb5\x4d\x6d\xe8\x81\x8d\x2e\x72\x84\xbe\x35\xea\x5b\x36\xf6\xea\xb1\x8a\xcf\xa7\x7d\x5c\xab\xcc\x79\xc1\x96\x3f\xca\xfb\xa8\x4c\xe0\xbc\x48\xcb\xdb\x7f\x1f\x92\x09\x9c\x17\x69\x79\xcd\xec\x43\x32\x81\x93\x20\x1d\xc5\xf4\x52\xfb\x54\xd1\xca\xbd\x55\xa5\x5b\x5b\x90\x2a\xb3\x82\xe2\xe6\x8c\x45\x1a\x4d\x29\xda\x3c\x7d\xc2\x76\xb8\x38\xfa\x03\x7b\xe6\xe2\xb8\x0f\x6c\xac\x8b\xe3\x3e\xb0\xfb\xfe\x16\x37\x2b\xff\x57\x63\x67\x2f\x29\xb3\xfd\xe1\x5d\x7d\xcb\xf6\x6a\xeb\x2b\xe5\xeb\x03\x46\xac\xfa\xaa\x39\xbe\xfa\xaa\xd9\xa4\x1e\xfb\x15\x00\x00\xff\xff\x15\x33\x61\x06\x58\x12\x00\x00") func migrationsSqlCockroach9SqlBytes() ([]byte, error) { @@ -385,6 +429,26 @@ func migrationsSqlShared1Sql() (*asset, error) { return a, nil } +var _migrationsSqlShared11Sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x7c\x90\xcd\x4e\x83\x50\x10\x85\xd7\xbd\x4f\x71\x96\x10\x65\x63\x8c\x9b\xae\x50\x6e\x13\x22\x42\x43\x21\x69\x57\x64\x5a\x26\xe5\x2a\x7f\xbd\x0c\xb6\xbc\xbd\xb1\xd6\x9f\x8d\x9d\xf5\x9c\x93\xef\x7c\x9e\x87\x9b\xc6\xec\x2d\x09\x23\xef\xd5\x53\xaa\xfd\x4c\x23\xf3\x1f\x23\x8d\x70\x81\x38\xc9\xa0\xd7\xe1\x2a\x5b\xa1\x9a\x4a\x4b\x45\x47\xa3\x54\x77\xc5\xab\x98\x62\x5b\xd3\xee\xad\x36\x83\xc0\x51\xb3\xc1\xec\x5b\x92\xd1\x32\xbe\xee\x9d\xec\xae\x22\xeb\x3c\xdc\xbb\xe7\x92\x38\x8f\x22\x2c\xd3\xf0\xc5\x4f\x37\x78\xd6\x9b\x5b\x35\xe3\x53\x6f\x2c\x0f\x05\x09\x30\x13\xd3\xf0\x20\xd4\xf4\xbf\xdf\x81\x5e\xf8\x79\x94\xa1\xed\x8e\x8e\xab\xdc\xb9\x52\x9e\x87\x66\x1a\x0e\x35\x2c\x1f\xc6\xcf\x2c\xa4\x62\x98\xb6\xe4\x13\xa4\xc3\x96\xd1\x52\xc3\xe5\xf7\x8a\x30\x0e\xf4\xfa\x0a\x77\x71\x26\x98\x90\xc4\x57\xc7\xe1\x0f\xe8\x85\xe2\x47\x59\xd0\x1d\x5b\x15\xa4\xc9\xf2\xa2\xec\xff\x9e\xb9\xfa\x08\x00\x00\xff\xff\x96\x66\xfa\x03\x6b\x01\x00\x00") + +func migrationsSqlShared11SqlBytes() ([]byte, error) { + return bindataRead( + _migrationsSqlShared11Sql, + "migrations/sql/shared/11.sql", + ) +} + +func migrationsSqlShared11Sql() (*asset, error) { + bytes, err := migrationsSqlShared11SqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "migrations/sql/shared/11.sql", size: 363, mode: os.FileMode(420), modTime: time.Unix(1585817202, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + var _migrationsSqlShared2Sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xac\x90\xb1\x0a\xc2\x30\x14\x45\xf7\x7e\xc5\xdd\xaa\x48\x97\x42\x27\xa7\x68\xea\x14\x5b\x29\xc9\x5c\x62\x1a\x4d\x05\x8d\xbc\xb4\x8a\x7f\x2f\x14\x04\x07\x45\x0b\xfd\x80\x73\x2e\xf7\x24\x09\x16\xe7\xf6\x48\xba\xb3\x50\xd7\x88\x09\x99\x57\x90\x6c\x25\x72\xb8\x47\x43\xba\xf6\xba\xef\x5c\x5a\x6b\x63\x6c\x08\x60\x9c\x23\xf4\xfb\x93\x35\x1d\x6e\x9a\x8c\xd3\x34\x4b\xb3\x6c\x8e\xa2\x94\x28\x94\x10\xe0\xf9\x86\x29\x21\x11\xc7\xcb\xef\x36\xb2\x07\xb2\xc1\x4d\xa5\x33\xbe\xb1\x53\xb9\x7c\xdb\x98\x91\xae\xe8\x3d\x22\xf7\xf7\xcb\xcf\x8c\xe0\x55\xb9\xc3\xba\x14\x6a\x5b\xbc\x86\xfe\xc8\x35\x8e\x1a\xaa\x8c\x43\x86\xf3\x1f\x91\x67\x00\x00\x00\xff\xff\xa3\x05\x9b\x27\x28\x02\x00\x00") func migrationsSqlShared2SqlBytes() ([]byte, error) { @@ -505,6 +569,26 @@ func migrationsSqlTests10_testSql() (*asset, error) { return a, nil } +var _migrationsSqlTests11_testSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x58\x4d\x6f\xe3\x36\x13\x3e\xaf\x7f\xc5\xc0\x17\x27\x78\xa5\xb5\x6c\xc7\xf1\xc7\x7b\x2a\xd0\x3d\x2c\x50\x64\x81\x6e\xb6\x3d\x14\x05\x41\x91\x23\x99\xb1\xcc\x51\x49\x2a\x4e\x1a\xe7\xbf\x17\xd4\xb7\xbd\x46\x76\x0b\x14\x69\x0f\xc9\x21\x20\x67\x34\x24\xe7\x79\x9e\x19\x4a\x0e\x43\xf8\xdf\x4e\xa5\x86\x3b\x84\x2f\xf9\xe0\xe3\xcd\xe7\x0f\x3f\xdf\xc2\xc7\x9b\xdb\x4f\xb0\x79\x94\x86\x33\x91\x29\xd4\x0e\x2e\x94\x0c\x80\x67\x19\xed\x51\x32\x41\xc6\x32\x32\x2a\x55\xda\x06\x50\x3d\xc1\x34\xdf\x61\x3b\xb1\x28\x0c\xba\x00\x0c\x4a\x65\x50\x38\x56\x18\x65\x03\x48\x0d\xd7\x8e\xb9\xc7\x1c\xad\xf7\xd9\x9c\xb4\xc5\x66\x6e\x05\xe5\x18\x00\xed\x35\x9a\x00\x72\xca\x94\x78\xf4\x71\x01\x38\xb2\xd5\xa0\x5e\xbd\x1c\x67\x94\x52\x6d\x25\xed\xb8\x70\xf6\x64\x77\x86\x0f\xb9\x32\x68\x19\x77\x01\x58\x14\x8e\x0c\x53\x12\xb5\x53\x89\x42\x53\x85\xde\xed\xb7\xb6\xfa\xdf\xec\xb4\x45\xcd\x50\xcb\x9c\x94\x76\x8c\x17\x6e\xc3\x76\xe8\x36\x24\xfd\x79\xff\x28\xd0\x36\xa9\x34\x33\x8a\xef\x7c\x7e\x56\xa5\x5a\xe9\x94\xf1\x2c\x0d\xa0\xb0\x68\x94\x4e\xa8\xb4\xa2\x64\x6d\xa6\xa5\xd7\x16\x55\x88\x4f\x3b\x00\x5e\x48\x85\x5a\x60\x00\x89\x21\xed\xc4\x86\x6b\x8d\x19\xf3\xd9\x15\x75\xa6\xe7\x1c\x16\xad\x55\xa4\x99\x3f\x86\x32\x28\x3d\x60\xd6\x35\xde\x13\xd8\x63\x2e\xb6\xe7\x16\x3e\x63\xff\x7a\xdd\x1d\x3a\x2e\xb9\xe3\x97\x83\x5f\x7e\xf8\xe9\xcb\x87\xcf\x03\x80\x8b\xd1\x64\x12\x56\x58\x8f\x02\x18\x6d\x9c\xcb\xd7\xe3\x71\x46\x82\x67\x1b\xb2\xee\x50\x1b\x52\xa2\x34\x43\xff\x84\xa5\x1d\xf6\x02\x78\x2c\x24\x26\xdf\x17\xea\x29\x20\xa3\xfe\x44\x26\x48\xe2\x41\xed\xf2\x4c\x09\x55\x2e\x53\x92\x75\x50\x92\x95\x03\x6f\x49\x88\x0e\x31\x37\x65\x1c\x6a\xe4\xb6\xb7\x47\x25\xa8\x9e\xc1\x51\xdf\x7d\x26\x9d\x94\xba\x85\x0e\x09\xf9\x59\xd4\xf9\x2b\x45\xf9\x27\x9e\x86\x5b\x7c\xb4\xc3\x35\xfc\xf6\xfb\x73\x6f\x01\xaf\x2a\x3f\xd5\xa4\xb1\x67\x2e\x8c\x9a\x1c\xba\xf1\xd4\xbb\x8c\x9d\xce\xaf\xab\xc1\x7c\x5a\x0e\xf2\x22\xce\x94\x68\xc2\xec\x7a\x3c\xde\xef\xf7\xef\xc9\x3c\xbe\xb7\x9b\x31\xcf\x55\x6f\xc1\x44\x84\x15\x79\xe3\x51\x00\xce\x14\xd8\xb9\x4a\x1d\x4c\xc6\x87\xfe\x74\x3a\xee\xc5\xc6\x67\x62\x9f\x86\x09\xd1\x70\x3d\x8c\xb9\x19\x3e\x8f\x2e\xff\x3f\xe8\x77\x85\xc1\xbb\xaa\x2d\x90\xe7\x65\x5a\x16\x88\xaf\x28\xc1\x9d\x57\x4d\xad\x9e\xba\x5b\x74\x3e\x94\x55\x11\x56\xca\x6f\x95\xf4\xae\x14\x52\x46\xa9\xd2\x61\x1d\x1a\x2a\x39\x0a\xe0\xe6\xd3\xaf\x17\x97\x01\x78\xaf\x2d\xe2\xbf\x77\x86\xba\x30\xe1\x42\x6c\x78\x96\xa1\x4e\x31\x80\x7b\x34\x65\xd1\xb7\x1d\xc2\x9f\xaf\x3e\x4d\xbf\xb0\xb3\x00\xec\x56\xe5\xad\x09\x25\xab\xbb\x92\xb0\x26\x39\x97\x52\xf7\xa0\x9f\x91\x92\x82\xf9\x76\x84\x0f\xae\xec\x50\xaa\xc5\xa4\xdc\xb2\xff\x34\x6b\x2a\xff\x04\x8e\xf6\xd8\xa3\x0a\x80\xe6\xec\xf5\xb4\xd3\x69\x05\x8e\x4f\xa1\x9e\x35\x55\x3f\x0a\x20\xe1\x99\xc5\xfa\x19\x9f\x40\x13\x6c\x4d\xd2\xc1\xdb\xa0\xfc\xf4\x5c\xbb\xcf\x30\xe1\xcd\xbc\x90\xdf\xa0\x40\xf8\xee\xa6\xdd\x7f\x08\xfb\x84\x8c\xf0\x2b\xd4\xbd\xb6\xeb\xfb\xe7\x68\xa9\x2c\xbd\x33\x9f\xe5\x29\x00\x2e\x4c\x75\xdb\xe0\xc3\xa9\x88\xff\x45\xd6\xaa\x54\xcb\x42\x79\x91\xc6\xd3\x23\x7a\x5a\xeb\x91\xa8\xfa\xd8\xb7\xaa\xfd\x84\x66\xb6\xe1\x5a\x66\x28\x8f\xe8\x2e\xaf\xf8\x8e\x3b\x83\x3b\xdc\xc5\x1e\xf6\x66\xc4\x12\x32\x01\xa0\x31\x64\x4e\x29\x6c\x38\xe1\x42\xa0\xb5\x55\x5b\xef\xac\x4d\xa3\x3f\x27\x85\x3d\xb7\xac\xb0\xfe\xba\x6a\xf6\xff\xfe\x02\x6b\xa0\xae\xda\xdf\xec\x3a\x8a\x1a\x74\x8f\xa1\xee\x9b\x7a\x4c\x35\xd5\x11\x86\x70\xbb\x41\xc8\x0d\xde\x2b\x2a\x2c\xc4\x19\x89\x2d\x28\x0b\x77\x85\x75\xe0\x08\x52\x74\x5e\x96\xa8\x52\x0d\xfe\xc2\x80\x3d\x99\xad\xd2\xe9\x8b\xbd\xad\x44\x02\x2e\xfc\x8b\x04\x77\x85\xe9\xb4\xf9\x55\x3f\x39\xae\xaf\x0a\xfd\x13\x32\x12\x32\x3b\xe6\xef\xf2\x0e\xd4\x7a\xd6\x94\x23\x17\x4e\xdd\x1f\x17\x40\xab\xfe\x16\xd9\xd6\xd2\x22\xc9\x94\x3c\x01\xd9\xaa\xb4\xd5\x77\xb9\xd4\x71\x57\x3f\xae\x87\x9e\xd6\xeb\x4d\x3a\xdb\xa8\x2f\xf5\xae\x72\xea\xbb\xaa\x4f\x27\xca\x9e\xa4\x9b\x75\x3a\x4b\x47\xfb\xcb\x22\x37\x98\x18\xb4\x9b\x37\xcc\x5f\x11\x73\xff\x6a\xf7\x06\xf8\x2b\x02\xee\x2f\xcb\x37\xc0\x5f\x11\xf0\x7c\x2b\xde\x14\xfe\xcf\x03\x0e\x61\x08\xd5\x31\x07\xd0\x07\x1f\xfc\xdf\x11\x01\x77\x4e\xb1\x38\xe3\x62\x9b\x29\xff\x82\xda\x63\xa2\xfb\x99\xe0\x72\x00\xed\x37\x2e\xf8\xcf\x5c\x31\x8f\xaf\xf8\x42\x8a\xab\xc5\x6a\x89\xf1\x2c\xba\x5a\xcc\xa7\xb3\x38\x92\xf1\x82\x2f\x57\xab\xd5\x7c\x29\xaf\xaf\x92\xd5\x94\x2f\xe3\xe9\x15\x26\xb3\x65\x34\x5f\xcc\x67\x2b\x4c\x66\xd3\xc5\xf5\x8c\xc7\xe5\xf7\xdb\x34\x9a\x2d\xc3\x68\x12\x4e\x56\x10\x4d\xd6\x51\xb4\x8e\xa2\xe6\x7d\xa1\xfd\xe5\xe5\x47\xda\xeb\xc1\x5f\x01\x00\x00\xff\xff\x20\xb9\x94\xb5\x8b\x11\x00\x00") + +func migrationsSqlTests11_testSqlBytes() ([]byte, error) { + return bindataRead( + _migrationsSqlTests11_testSql, + "migrations/sql/tests/11_test.sql", + ) +} + +func migrationsSqlTests11_testSql() (*asset, error) { + bytes, err := migrationsSqlTests11_testSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "migrations/sql/tests/11_test.sql", size: 4491, mode: os.FileMode(420), modTime: time.Unix(1585818782, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + var _migrationsSqlTests1_testSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xdc\x93\x41\x4b\x03\x31\x10\x85\xcf\xcd\xaf\x98\x5b\x76\x31\x7b\xa8\x57\x4f\x82\x3d\x14\x64\x0b\xb6\xd5\x63\x18\x92\x69\x36\x60\x93\x3a\x93\x45\x44\xfc\xef\xd2\xcd\xa2\x9e\xbc\xb7\x87\xc0\xbc\xf7\xe0\x3d\xbe\x43\xba\x0e\x6e\x8e\x31\x30\x16\x82\xfd\x49\xad\xfb\xed\xea\x69\x07\xeb\x7e\xb7\x51\x8b\xe1\xc3\x33\xda\x8c\x63\x19\x6e\x2d\x3a\x47\x22\xd0\x48\x0c\x09\xcb\xc8\x64\x80\xe9\x6d\x24\x29\x36\xfa\x9f\x9b\xbc\xc5\x62\xc0\xbd\x46\x4a\x35\x10\x97\x4f\x64\x20\x30\xa6\x73\x3a\xcb\x43\xe6\xa3\xf5\x58\xd0\x80\x90\x48\xcc\x69\x52\xad\x7a\xbe\x7f\xdc\xaf\xb6\x6a\xd1\xe8\x65\x27\x31\x68\x03\x7a\xd9\xcd\xe5\xda\x40\xbf\x79\x69\xda\xc9\xab\x13\x35\x9f\x4a\xeb\x39\xef\xfc\x5a\xe7\xf7\xf9\xa5\xdb\x3b\xf5\x0f\x1c\xd3\x81\x49\x86\x2b\xa5\x73\xd9\xd3\x95\xa2\xe5\xe8\xdd\x45\xa3\xfd\xfd\x7f\x0f\xf9\x3d\xa9\xef\x00\x00\x00\xff\xff\x72\x0a\x2b\x58\x91\x03\x00\x00") func migrationsSqlTests1_testSqlBytes() ([]byte, error) { @@ -737,6 +821,8 @@ func AssetNames() []string { // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ + "migrations/sql/cockroach/10.sql": migrationsSqlCockroach10Sql, + "migrations/sql/cockroach/11.sql": migrationsSqlCockroach11Sql, "migrations/sql/cockroach/9.sql": migrationsSqlCockroach9Sql, "migrations/sql/mysql/.gitkeep": migrationsSqlMysqlGitkeep, "migrations/sql/mysql/10.sql": migrationsSqlMysql10Sql, @@ -751,12 +837,14 @@ var _bindata = map[string]func() (*asset, error){ "migrations/sql/postgres/7.sql": migrationsSqlPostgres7Sql, "migrations/sql/postgres/9.sql": migrationsSqlPostgres9Sql, "migrations/sql/shared/1.sql": migrationsSqlShared1Sql, + "migrations/sql/shared/11.sql": migrationsSqlShared11Sql, "migrations/sql/shared/2.sql": migrationsSqlShared2Sql, "migrations/sql/shared/3.sql": migrationsSqlShared3Sql, "migrations/sql/shared/4.sql": migrationsSqlShared4Sql, "migrations/sql/shared/8.sql": migrationsSqlShared8Sql, "migrations/sql/tests/.gitkeep": migrationsSqlTestsGitkeep, "migrations/sql/tests/10_test.sql": migrationsSqlTests10_testSql, + "migrations/sql/tests/11_test.sql": migrationsSqlTests11_testSql, "migrations/sql/tests/1_test.sql": migrationsSqlTests1_testSql, "migrations/sql/tests/2_test.sql": migrationsSqlTests2_testSql, "migrations/sql/tests/3_test.sql": migrationsSqlTests3_testSql, @@ -812,7 +900,9 @@ var _bintree = &bintree{nil, map[string]*bintree{ "migrations": &bintree{nil, map[string]*bintree{ "sql": &bintree{nil, map[string]*bintree{ "cockroach": &bintree{nil, map[string]*bintree{ - "9.sql": &bintree{migrationsSqlCockroach9Sql, map[string]*bintree{}}, + "10.sql": &bintree{migrationsSqlCockroach10Sql, map[string]*bintree{}}, + "11.sql": &bintree{migrationsSqlCockroach11Sql, map[string]*bintree{}}, + "9.sql": &bintree{migrationsSqlCockroach9Sql, map[string]*bintree{}}, }}, "mysql": &bintree{nil, map[string]*bintree{ ".gitkeep": &bintree{migrationsSqlMysqlGitkeep, map[string]*bintree{}}, @@ -831,15 +921,17 @@ var _bintree = &bintree{nil, map[string]*bintree{ "9.sql": &bintree{migrationsSqlPostgres9Sql, map[string]*bintree{}}, }}, "shared": &bintree{nil, map[string]*bintree{ - "1.sql": &bintree{migrationsSqlShared1Sql, map[string]*bintree{}}, - "2.sql": &bintree{migrationsSqlShared2Sql, map[string]*bintree{}}, - "3.sql": &bintree{migrationsSqlShared3Sql, map[string]*bintree{}}, - "4.sql": &bintree{migrationsSqlShared4Sql, map[string]*bintree{}}, - "8.sql": &bintree{migrationsSqlShared8Sql, map[string]*bintree{}}, + "1.sql": &bintree{migrationsSqlShared1Sql, map[string]*bintree{}}, + "11.sql": &bintree{migrationsSqlShared11Sql, map[string]*bintree{}}, + "2.sql": &bintree{migrationsSqlShared2Sql, map[string]*bintree{}}, + "3.sql": &bintree{migrationsSqlShared3Sql, map[string]*bintree{}}, + "4.sql": &bintree{migrationsSqlShared4Sql, map[string]*bintree{}}, + "8.sql": &bintree{migrationsSqlShared8Sql, map[string]*bintree{}}, }}, "tests": &bintree{nil, map[string]*bintree{ ".gitkeep": &bintree{migrationsSqlTestsGitkeep, map[string]*bintree{}}, "10_test.sql": &bintree{migrationsSqlTests10_testSql, map[string]*bintree{}}, + "11_test.sql": &bintree{migrationsSqlTests11_testSql, map[string]*bintree{}}, "1_test.sql": &bintree{migrationsSqlTests1_testSql, map[string]*bintree{}}, "2_test.sql": &bintree{migrationsSqlTests2_testSql, map[string]*bintree{}}, "3_test.sql": &bintree{migrationsSqlTests3_testSql, map[string]*bintree{}}, diff --git a/oauth2/x_fosite_migrations_test.go b/oauth2/x_fosite_migrations_test.go index c51d671bd65..7c00ed5af60 100644 --- a/oauth2/x_fosite_migrations_test.go +++ b/oauth2/x_fosite_migrations_test.go @@ -6,16 +6,18 @@ import ( "testing" "github.com/jmoiron/sqlx" + "github.com/pkg/errors" "github.com/stretchr/testify/require" "github.com/ory/fosite" + "github.com/ory/x/dbal" + "github.com/ory/x/dbal/migratest" + "github.com/ory/hydra/client" "github.com/ory/hydra/consent" "github.com/ory/hydra/internal" "github.com/ory/hydra/oauth2" "github.com/ory/hydra/x" - "github.com/ory/x/dbal" - "github.com/ory/x/dbal/migratest" ) func TestXXMigrations(t *testing.T) { @@ -52,6 +54,7 @@ func TestXXMigrations(t *testing.T) { require.Error(t, err) return } + _, err := s.GetAccessTokenSession(context.Background(), sig, oauth2.NewSession("")) require.NoError(t, err) _, err = s.GetRefreshTokenSession(context.Background(), sig, oauth2.NewSession("")) @@ -64,6 +67,10 @@ func TestXXMigrations(t *testing.T) { _, err = s.GetPKCERequestSession(context.Background(), sig, oauth2.NewSession("")) require.NoError(t, err) } + + if k >= 11 { + require.True(t, errors.Is(s.ClientAssertionJWTValid(context.Background(), sig), fosite.ErrJTIKnown), "%+v", err) + } }) }, ) diff --git a/x/clean_sql.go b/x/clean_sql.go index 98dcbd64e3c..a9355a9217c 100644 --- a/x/clean_sql.go +++ b/x/clean_sql.go @@ -21,6 +21,7 @@ func CleanSQL(t *testing.T, db *sqlx.DB) { "hydra_oauth2_authentication_session", "hydra_oauth2_obfuscated_authentication_session", "hydra_oauth2_logout_request", + "hydra_oauth2_jti_blacklist", "hydra_jwk", "hydra_client", // Migrations