diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0f1bcd4073b..fd87edb8c50 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -94,7 +94,7 @@ jobs: GOGC: 100 with: args: --timeout 10m0s - version: v1.55.2 + version: v1.61.0 skip-pkg-cache: true - name: Run go-acc (tests) run: | diff --git a/.github/workflows/pm.yml b/.github/workflows/pm.yml new file mode 100644 index 00000000000..0c69d71b706 --- /dev/null +++ b/.github/workflows/pm.yml @@ -0,0 +1,29 @@ +name: Synchronize with product board + +on: + issues: + types: + - opened + pull_request: + types: + - opened + - ready_for_review + +jobs: + automate: + if: github.event.pull_request.head.repo.fork == false + name: Add issue to project + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: ory-corp/planning-automation-action@v0.1 + with: + organization: ory-corp + project: 5 + token: ${{ secrets.ORY_BOT_PAT }} + todoLabel: "Needs Triage" + statusName: Status + statusValue: "Needs Triage" + includeEffort: "false" + monthlyMilestoneName: Roadmap Monthly + quarterlyMilestoneName: Roadmap diff --git a/.golangci.yml b/.golangci.yml index 00ee1f9963c..2dff48664e4 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -8,11 +8,9 @@ linters: - goimports disable: - ineffassign - - deadcode - unused - - structcheck -run: - skip-files: +issues: + exclude-files: - ".+_test.go" - ".+_test_.+.go" diff --git a/.schema/config.schema.json b/.schema/config.schema.json index 3c078b880d3..bc1d1476c08 100644 --- a/.schema/config.schema.json +++ b/.schema/config.schema.json @@ -40,7 +40,7 @@ }, "mode": { "type": "integer", - "description": "Mode of unix socket in numeric form", + "description": "Mode of unix socket in numeric form, base 10.", "default": 493, "minimum": 0, "maximum": 511 @@ -1168,7 +1168,7 @@ "examples": ["cpu"] }, "tracing": { - "$ref": "https://raw.githubusercontent.com/ory/x/v0.0.612-0.20240130132700-6275e3f1ad0d/otelx/config.schema.json" + "$ref": "https://raw.githubusercontent.com/ory/x/v0.0.612/otelx/config.schema.json" }, "sqa": { "type": "object", diff --git a/Makefile b/Makefile index 0d2e186cf8b..75b912e0521 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ export PATH := .bin:${PATH} export PWD := $(shell pwd) export IMAGE_TAG := $(if $(IMAGE_TAG),$(IMAGE_TAG),latest) -GOLANGCI_LINT_VERSION = 1.55.2 +GOLANGCI_LINT_VERSION = 1.61.0 GO_DEPENDENCIES = github.com/ory/go-acc \ github.com/golang/mock/mockgen \ diff --git a/client/validator.go b/client/validator.go index 8be6512bc70..bedd9cde671 100644 --- a/client/validator.go +++ b/client/validator.go @@ -7,6 +7,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net/url" "strings" @@ -233,6 +234,7 @@ func (v *Validator) ValidateSectorIdentifierURL(ctx context.Context, location st return errorsx.WithStack(ErrInvalidClientMetadata.WithDebug(fmt.Sprintf("Unable to connect to URL set by sector_identifier_uri: %s", err))) } defer response.Body.Close() + response.Body = io.NopCloser(io.LimitReader(response.Body, 5<<20 /* 5 MiB */)) var urls []string if err := json.NewDecoder(response.Body).Decode(&urls); err != nil { diff --git a/cmd/.snapshots/TestUpdateClient-case=updates_from_file-file=from_disk.json b/cmd/.snapshots/TestUpdateClient-case=updates_from_file-file=from_disk.json new file mode 100644 index 00000000000..e9a42532a2f --- /dev/null +++ b/cmd/.snapshots/TestUpdateClient-case=updates_from_file-file=from_disk.json @@ -0,0 +1,24 @@ +{ + "client_name": "updated through file from disk", + "client_secret_expires_at": 0, + "client_uri": "", + "grant_types": [ + "implicit" + ], + "jwks": {}, + "logo_uri": "", + "metadata": {}, + "owner": "", + "policy_uri": "", + "request_object_signing_alg": "RS256", + "response_types": [ + "code" + ], + "scope": "offline_access offline openid", + "skip_consent": false, + "skip_logout_consent": false, + "subject_type": "public", + "token_endpoint_auth_method": "client_secret_basic", + "tos_uri": "", + "userinfo_signed_response_alg": "none" +} diff --git a/cmd/.snapshots/TestUpdateClient-case=updates_from_file-file=stdin.json b/cmd/.snapshots/TestUpdateClient-case=updates_from_file-file=stdin.json new file mode 100644 index 00000000000..4491f0eed55 --- /dev/null +++ b/cmd/.snapshots/TestUpdateClient-case=updates_from_file-file=stdin.json @@ -0,0 +1,24 @@ +{ + "client_name": "updated through file stdin", + "client_secret_expires_at": 0, + "client_uri": "", + "grant_types": [ + "implicit" + ], + "jwks": {}, + "logo_uri": "", + "metadata": {}, + "owner": "", + "policy_uri": "", + "request_object_signing_alg": "RS256", + "response_types": [ + "code" + ], + "scope": "offline_access offline openid", + "skip_consent": false, + "skip_logout_consent": false, + "subject_type": "public", + "token_endpoint_auth_method": "client_secret_basic", + "tos_uri": "", + "userinfo_signed_response_alg": "none" +} diff --git a/cmd/cli/handler_migrate.go b/cmd/cli/handler_migrate.go index a4c2dd4885d..dd064ab7170 100644 --- a/cmd/cli/handler_migrate.go +++ b/cmd/cli/handler_migrate.go @@ -5,7 +5,6 @@ package cli import ( "bytes" - "context" "fmt" "io" "io/fs" @@ -13,7 +12,6 @@ import ( "path/filepath" "regexp" "strings" - "time" "github.com/ory/x/popx" "github.com/ory/x/servicelocatorx" @@ -22,8 +20,6 @@ import ( "github.com/ory/x/configx" - "github.com/ory/x/errorsx" - "github.com/ory/x/cmdx" "github.com/spf13/cobra" @@ -317,54 +313,20 @@ func (h *MigrateHandler) makePersister(cmd *cobra.Command, args []string) (p per return d.Persister(), nil } -func (h *MigrateHandler) MigrateSQL(cmd *cobra.Command, args []string) (err error) { +func (h *MigrateHandler) MigrateSQLUp(cmd *cobra.Command, args []string) (err error) { p, err := h.makePersister(cmd, args) if err != nil { return err } - conn := p.Connection(context.Background()) - if conn == nil { - _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Migrations can only be executed against a SQL-compatible driver but DSN is not a SQL source.") - return cmdx.FailSilently(cmd) - } - - if err := conn.Open(); err != nil { - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not open the database connection:\n%+v\n", err) - return cmdx.FailSilently(cmd) - } - - // convert migration tables - if err := p.PrepareMigration(context.Background()); err != nil { - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not convert the migration table:\n%+v\n", err) - return cmdx.FailSilently(cmd) - } - - // print migration status - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "The following migration is planned:") + return popx.MigrateSQLUp(cmd, p) +} - status, err := p.MigrationStatus(context.Background()) +func (h *MigrateHandler) MigrateSQLDown(cmd *cobra.Command, args []string) (err error) { + p, err := h.makePersister(cmd, args) if err != nil { - fmt.Fprintf(cmd.ErrOrStderr(), "Could not get the migration status:\n%+v\n", errorsx.WithStack(err)) - return cmdx.FailSilently(cmd) - } - _ = status.Write(os.Stdout) - - if !flagx.MustGetBool(cmd, "yes") { - _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "To skip the next question use flag --yes (at your own risk).") - if !cmdx.AskForConfirmation("Do you wish to execute this migration plan?", nil, nil) { - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Migration aborted.") - return nil - } - } - - // apply migrations - if err := p.MigrateUp(context.Background()); err != nil { - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not apply migrations:\n%+v\n", errorsx.WithStack(err)) - return cmdx.FailSilently(cmd) + return err } - - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Successfully applied migrations!") - return nil + return popx.MigrateSQLDown(cmd, p) } func (h *MigrateHandler) MigrateStatus(cmd *cobra.Command, args []string) error { @@ -372,41 +334,5 @@ func (h *MigrateHandler) MigrateStatus(cmd *cobra.Command, args []string) error if err != nil { return err } - conn := p.Connection(context.Background()) - if conn == nil { - _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Migrations can only be checked against a SQL-compatible driver but DSN is not a SQL source.") - return cmdx.FailSilently(cmd) - } - - if err := conn.Open(); err != nil { - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not open the database connection:\n%+v\n", err) - return cmdx.FailSilently(cmd) - } - - block := flagx.MustGetBool(cmd, "block") - ctx := cmd.Context() - s, err := p.MigrationStatus(ctx) - if err != nil { - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get migration status: %+v\n", err) - return cmdx.FailSilently(cmd) - } - - for block && s.HasPending() { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Waiting for migrations to finish...\n") - for _, m := range s { - if m.State == popx.Pending { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), " - %s\n", m.Name) - } - } - time.Sleep(time.Second) - s, err = p.MigrationStatus(ctx) - if err != nil { - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get migration status: %+v\n", err) - return cmdx.FailSilently(cmd) - } - } - - cmdx.PrintTable(cmd, s) - return nil - + return popx.MigrateStatus(cmd, p) } diff --git a/cmd/cmd_create_client.go b/cmd/cmd_create_client.go index 4c3105283ce..4d72b22f1ab 100644 --- a/cmd/cmd_create_client.go +++ b/cmd/cmd_create_client.go @@ -17,6 +17,8 @@ import ( ) const ( + flagFile = "file" + flagClientAccessTokenStrategy = "access-token-strategy" flagClientAllowedCORSOrigin = "allowed-cors-origin" flagClientAudience = "audience" @@ -87,7 +89,10 @@ To encrypt an auto-generated OAuth2 Client Secret, use flags ` + "`--pgp-key`" + } secret := flagx.MustGetString(cmd, flagClientSecret) - cl := clientFromFlags(cmd) + cl, err := clientFromFlags(cmd) + if err != nil { + return err + } cl.ClientId = pointerx.Ptr(flagx.MustGetString(cmd, flagClientId)) //nolint:bodyclose diff --git a/cmd/cmd_helper_client.go b/cmd/cmd_helper_client.go index 70fa6f98bcc..ca28fafa12a 100644 --- a/cmd/cmd_helper_client.go +++ b/cmd/cmd_helper_client.go @@ -5,6 +5,8 @@ package cmd import ( "encoding/json" + "fmt" + "os" "strings" "github.com/spf13/cobra" @@ -16,7 +18,24 @@ import ( "github.com/ory/x/pointerx" ) -func clientFromFlags(cmd *cobra.Command) hydra.OAuth2Client { +func clientFromFlags(cmd *cobra.Command) (hydra.OAuth2Client, error) { + if filename := flagx.MustGetString(cmd, flagFile); filename != "" { + src := cmd.InOrStdin() + if filename != "-" { + f, err := os.Open(filename) + if err != nil { + return hydra.OAuth2Client{}, fmt.Errorf("unable to open file %q: %w", filename, err) + } + defer f.Close() + src = f + } + client := hydra.OAuth2Client{} + if err := json.NewDecoder(src).Decode(&client); err != nil { + return hydra.OAuth2Client{}, fmt.Errorf("unable to decode JSON: %w", err) + } + return client, nil + } + return hydra.OAuth2Client{ AccessTokenStrategy: pointerx.Ptr(flagx.MustGetString(cmd, flagClientAccessTokenStrategy)), AllowedCorsOrigins: flagx.MustGetStringSlice(cmd, flagClientAllowedCORSOrigin), @@ -47,7 +66,7 @@ func clientFromFlags(cmd *cobra.Command) hydra.OAuth2Client { SubjectType: pointerx.Ptr(flagx.MustGetString(cmd, flagClientSubjectType)), TokenEndpointAuthMethod: pointerx.Ptr(flagx.MustGetString(cmd, flagClientTokenEndpointAuthMethod)), TosUri: pointerx.Ptr(flagx.MustGetString(cmd, flagClientTOSURI)), - } + }, nil } func registerEncryptFlags(flags *pflag.FlagSet) { @@ -58,6 +77,8 @@ func registerEncryptFlags(flags *pflag.FlagSet) { } func registerClientFlags(flags *pflag.FlagSet) { + flags.String(flagFile, "", "Read a JSON file representing a client from this location. If set, the other client flags are ignored.") + flags.String(flagClientMetadata, "{}", "Metadata is an arbitrary JSON String of your choosing.") flags.String(flagClientOwner, "", "The owner of this client, typically email addresses or a user ID.") flags.StringSlice(flagClientContact, nil, "A list representing ways to contact people responsible for this client, typically email addresses.") diff --git a/cmd/cmd_import_client_test.go b/cmd/cmd_import_client_test.go index 9e32dd2907f..cc82b1aa69e 100644 --- a/cmd/cmd_import_client_test.go +++ b/cmd/cmd_import_client_test.go @@ -8,6 +8,7 @@ import ( "context" "encoding/json" "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -23,14 +24,12 @@ import ( func writeTempFile(t *testing.T, contents interface{}) string { t.Helper() - ij, err := json.Marshal(contents) - require.NoError(t, err) - f, err := os.CreateTemp(t.TempDir(), "") - require.NoError(t, err) - _, err = f.Write(ij) + fn := filepath.Join(t.TempDir(), "content.json") + f, err := os.Create(fn) require.NoError(t, err) + require.NoError(t, json.NewEncoder(f).Encode(contents)) require.NoError(t, f.Close()) - return f.Name() + return fn } func TestImportClient(t *testing.T) { @@ -38,8 +37,8 @@ func TestImportClient(t *testing.T) { c := cmd.NewImportClientCmd() reg := setup(t, c) - file1 := writeTempFile(t, []hydra.OAuth2Client{{Scope: pointerx.String("foo")}, {Scope: pointerx.String("bar"), ClientSecret: pointerx.String("some-secret")}}) - file2 := writeTempFile(t, []hydra.OAuth2Client{{Scope: pointerx.String("baz")}, {Scope: pointerx.String("zab"), ClientSecret: pointerx.String("some-secret")}}) + file1 := writeTempFile(t, []hydra.OAuth2Client{{Scope: pointerx.Ptr("foo")}, {Scope: pointerx.Ptr("bar"), ClientSecret: pointerx.Ptr("some-secret")}}) + file2 := writeTempFile(t, []hydra.OAuth2Client{{Scope: pointerx.Ptr("baz")}, {Scope: pointerx.Ptr("zab"), ClientSecret: pointerx.Ptr("some-secret")}}) t.Run("case=imports clients from single file", func(t *testing.T) { actual := gjson.Parse(cmdx.ExecNoErr(t, c, file1)) @@ -77,7 +76,7 @@ func TestImportClient(t *testing.T) { t.Run("case=imports clients from multiple files and stdin", func(t *testing.T) { var stdin bytes.Buffer - require.NoError(t, json.NewEncoder(&stdin).Encode([]hydra.OAuth2Client{{Scope: pointerx.String("oof")}, {Scope: pointerx.String("rab"), ClientSecret: pointerx.String("some-secret")}})) + require.NoError(t, json.NewEncoder(&stdin).Encode([]hydra.OAuth2Client{{Scope: pointerx.Ptr("oof")}, {Scope: pointerx.Ptr("rab"), ClientSecret: pointerx.Ptr("some-secret")}})) stdout, _, err := cmdx.Exec(t, c, &stdin, file1, file2) require.NoError(t, err) @@ -93,7 +92,7 @@ func TestImportClient(t *testing.T) { }) t.Run("case=performs appropriate error reporting", func(t *testing.T) { - file3 := writeTempFile(t, []hydra.OAuth2Client{{ClientSecret: pointerx.String("short")}}) + file3 := writeTempFile(t, []hydra.OAuth2Client{{ClientSecret: pointerx.Ptr("short")}}) stdout, stderr, err := cmdx.Exec(t, c, nil, file1, file3) require.Error(t, err) actual := gjson.Parse(stdout) diff --git a/cmd/cmd_perform_authorization_code.go b/cmd/cmd_perform_authorization_code.go index a376f7a0b73..bb996131a3a 100644 --- a/cmd/cmd_perform_authorization_code.go +++ b/cmd/cmd_perform_authorization_code.go @@ -4,39 +4,106 @@ package cmd import ( + "bytes" "context" "crypto/rand" "crypto/rsa" "crypto/tls" + "encoding/json" "fmt" "html/template" + "io" "net/http" + "net/url" "os" "strconv" "strings" "time" - "github.com/ory/hydra/v2/cmd/cliclient" - - "github.com/pkg/errors" - - "github.com/ory/graceful" - "github.com/julienschmidt/httprouter" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/toqueteos/webbrowser" "golang.org/x/oauth2" + "github.com/ory/graceful" + openapi "github.com/ory/hydra-client-go/v2" + "github.com/ory/hydra/v2/cmd/cliclient" "github.com/ory/x/cmdx" "github.com/ory/x/flagx" + "github.com/ory/x/pointerx" "github.com/ory/x/randx" "github.com/ory/x/tlsx" "github.com/ory/x/urlx" ) +var tokenUserLogin = template.Must(template.New("").Parse(` + +

Login step

+
+ + + Remember login
+ Revoke previous consents
+ + +
+{{ if .Skip }} + user authenticated, could skip login UI. +{{ else }} + User unknown. +{{ end }} +
+

Complete login request

+
{{ .Raw }}
+ +`)) + +var tokenUserConsent = template.Must(template.New("").Parse(` + +

Consent step

+
+ + {{ if not .Audiences }} + No token audiences requested. + {{ else }} +

Requested audiences:

+ + {{ end }} + {{ if not .Scopes }} + No scopes requested. + {{ else }} +

Requested scopes:

+ {{ range .Scopes }} + {{ . }}
+ {{ end }} + {{ end }} +
+ Remember consent
+ + +
+{{ if .Skip }} + Consent established, could skip consent UI. +{{ else }} + No previous matching consent found, or client has requested re-consent. +{{ end }} +
+

Previous consents for this login session ({{ .SessionID }})

+
{{ .PreviousConsents }}
+
+

Complete consent request

+
{{ .Raw }}
+ +`)) + var tokenUserWelcome = template.Must(template.New("").Parse(` -

Welcome to the exemplary OAuth 2.0 Consumer!

+

Welcome to the example OAuth 2.0 Consumer!

This is an example app which emulates an OAuth 2.0 consumer application. Usually, this would be your web or mobile application and would use an OAuth 2.0 or OpenID Connect library.

@@ -63,7 +130,7 @@ var tokenUserResult = template.Must(template.New("").Parse(` {{ if .DisplayBackButton }} @@ -76,8 +143,8 @@ func NewPerformAuthorizationCodeCmd() *cobra.Command { cmd := &cobra.Command{ Use: "authorization-code", Example: "{{ .CommandPath }} --client-id ... --client-secret ...", - Short: "An exemplary OAuth 2.0 Client performing the OAuth 2.0 Authorize Code Flow", - Long: `Starts an exemplary web server that acts as an OAuth 2.0 Client performing the Authorize Code Flow. + Short: "Example OAuth 2.0 Client performing the OAuth 2.0 Authorize Code Flow", + Long: `Starts an example web server that acts as an OAuth 2.0 Client performing the Authorize Code Flow. This command will help you to see if Ory Hydra has been configured properly. This command must not be used for anything else than manual testing or demo purposes. The server will terminate on error @@ -90,7 +157,6 @@ and success, unless if the --no-shutdown flag is provided.`, endpoint = cliclient.GetOAuth2URLOverride(cmd, endpoint) - ctx := context.WithValue(cmd.Context(), oauth2.HTTPClient, client) isSSL := flagx.MustGetBool(cmd, "https") port := flagx.MustGetInt(cmd, "port") scopes := flagx.MustGetStringSlice(cmd, "scope") @@ -101,6 +167,8 @@ and success, unless if the --no-shutdown flag is provided.`, tokenUrl := flagx.MustGetString(cmd, "token-url") audience := flagx.MustGetStringSlice(cmd, "audience") noShutdown := flagx.MustGetBool(cmd, "no-shutdown") + skip := flagx.MustGetBool(cmd, "skip") + responseMode := flagx.MustGetString(cmd, "response-mode") clientID := flagx.MustGetString(cmd, "client-id") if clientID == "" { @@ -150,32 +218,31 @@ and success, unless if the --no-shutdown flag is provided.`, nonce, err := randx.RuneSequence(24, randx.AlphaLower) cmdx.Must(err, "Could not generate random state: %s", err) - authCodeURL := conf.AuthCodeURL( - state, - oauth2.SetAuthURLParam("audience", strings.Join(audience, " ")), - oauth2.SetAuthURLParam("nonce", string(nonce)), - oauth2.SetAuthURLParam("prompt", strings.Join(prompt, " ")), - oauth2.SetAuthURLParam("max_age", strconv.Itoa(maxAge)), - ) + opts := []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("nonce", string(nonce))} + if len(audience) > 0 { + opts = append(opts, oauth2.SetAuthURLParam("audience", strings.Join(audience, " "))) + } + if len(prompt) > 0 { + opts = append(opts, oauth2.SetAuthURLParam("prompt", strings.Join(prompt, " "))) + } + if maxAge >= 0 { + opts = append(opts, oauth2.SetAuthURLParam("max_age", strconv.Itoa(maxAge))) + } + if responseMode != "" { + opts = append(opts, oauth2.SetAuthURLParam("response_mode", responseMode)) + } + + authCodeURL := conf.AuthCodeURL(state, opts...) return authCodeURL, state } authCodeURL, state := generateAuthCodeURL() - if !flagx.MustGetBool(cmd, "no-open") { - _ = webbrowser.Open(serverLocation) // ignore errors - } - - _, _ = fmt.Fprintln(os.Stderr, "Setting up home route on "+serverLocation) - _, _ = fmt.Fprintln(os.Stderr, "Setting up callback listener on "+serverLocation+"callback") - _, _ = fmt.Fprintln(os.Stderr, "Press ctrl + c on Linux / Windows or cmd + c on OSX to end the process.") - _, _ = fmt.Fprintf(os.Stderr, "If your browser does not open automatically, navigate to:\n\n\t%s\n\n", serverLocation) - r := httprouter.New() var tlsc *tls.Config if isSSL { key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "Unable to generate RSA key pair: %s", err) + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Unable to generate RSA key pair: %s", err) return cmdx.FailSilently(cmd) } @@ -196,14 +263,6 @@ and success, unless if the --no-shutdown flag is provided.`, defer cancel() _ = server.Shutdown(ctx) } - var onDone = func() { - if !noShutdown { - go shutdown() - } else { - // regenerate because we don't want to shutdown and we don't want to reuse nonce & state - authCodeURL, state = generateAuthCodeURL() - } - } r.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { _ = tokenUserWelcome.Execute(w, &struct{ URL string }{URL: authCodeURL}) @@ -213,72 +272,39 @@ and success, unless if the --no-shutdown flag is provided.`, http.Redirect(w, r, authCodeURL, http.StatusFound) }) - type ed struct { - Name string - Description string - Hint string - Debug string + rt := router{ + cl: client, + skip: skip, + cmd: cmd, + state: &state, + conf: &conf, + onDone: func() { + if !noShutdown { + go shutdown() + } else { + // regenerate because we don't want to shutdown and we don't want to reuse nonce & state + authCodeURL, state = generateAuthCodeURL() + } + }, + serverLocation: serverLocation, + noShutdown: noShutdown, } - r.GET("/callback", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - if len(r.URL.Query().Get("error")) > 0 { - _, _ = fmt.Fprintf(os.Stderr, "Got error: %s\n", r.URL.Query().Get("error_description")) - - w.WriteHeader(http.StatusInternalServerError) - _ = tokenUserError.Execute(w, &ed{ - Name: r.URL.Query().Get("error"), - Description: r.URL.Query().Get("error_description"), - Hint: r.URL.Query().Get("error_hint"), - Debug: r.URL.Query().Get("error_debug"), - }) - - onDone() - return - } - - if r.URL.Query().Get("state") != string(state) { - _, _ = fmt.Fprintf(os.Stderr, "States do not match. Expected %s, got %s\n", string(state), r.URL.Query().Get("state")) + r.GET("/login", rt.loginGET) + r.POST("/login", rt.loginPOST) + r.GET("/consent", rt.consentGET) + r.POST("/consent", rt.consentPOST) + r.GET("/callback", rt.callback) + r.POST("/callback", rt.callbackPOSTForm) - w.WriteHeader(http.StatusInternalServerError) - _ = tokenUserError.Execute(w, &ed{ - Name: "States do not match", - Description: "Expected state " + string(state) + " but got " + r.URL.Query().Get("state"), - }) - onDone() - return - } - - code := r.URL.Query().Get("code") - token, err := conf.Exchange(ctx, code) - if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "Unable to exchange code for token: %s\n", err) - - w.WriteHeader(http.StatusInternalServerError) - _ = tokenUserError.Execute(w, &ed{ - Name: err.Error(), - }) - onDone() - return - } + if !flagx.MustGetBool(cmd, "no-open") { + _ = webbrowser.Open(serverLocation) // ignore errors + } - cmdx.PrintRow(cmd, outputOAuth2Token(*token)) - _ = tokenUserResult.Execute(w, struct { - AccessToken string - RefreshToken string - Expiry string - IDToken string - BackURL string - DisplayBackButton bool - }{ - AccessToken: token.AccessToken, - RefreshToken: token.RefreshToken, - Expiry: token.Expiry.Format(time.RFC1123), - IDToken: fmt.Sprintf("%s", token.Extra("id_token")), - BackURL: serverLocation, - DisplayBackButton: noShutdown, - }) - onDone() - }) + _, _ = fmt.Fprintln(rt.cmd.ErrOrStderr(), "Setting up home route on "+serverLocation) + _, _ = fmt.Fprintln(rt.cmd.ErrOrStderr(), "Setting up callback listener on "+serverLocation+"callback") + _, _ = fmt.Fprintln(rt.cmd.ErrOrStderr(), "Press ctrl + c on Linux / Windows or cmd + c on OSX to end the process.") + _, _ = fmt.Fprintf(rt.cmd.ErrOrStderr(), "If your browser does not open automatically, navigate to:\n\n\t%s\n\n", serverLocation) if isSSL { err = server.ListenAndServeTLS("", "") @@ -300,7 +326,7 @@ and success, unless if the --no-shutdown flag is provided.`, cmd.Flags().IntP("port", "p", 4446, "The port on which the server should run") cmd.Flags().StringSlice("scope", []string{"offline", "openid"}, "Request OAuth2 scope") cmd.Flags().StringSlice("prompt", []string{}, "Set the OpenID Connect prompt parameter") - cmd.Flags().Int("max-age", 0, "Set the OpenID Connect max_age parameter") + cmd.Flags().Int("max-age", -1, "Set the OpenID Connect max_age parameter. -1 means no max_age parameter will be used.") cmd.Flags().Bool("no-shutdown", false, "Do not terminate on success/error. State and nonce will be regenerated when auth flow has completed (either due to an error or success).") cmd.Flags().String("client-id", os.Getenv("OAUTH2_CLIENT_ID"), "Use the provided OAuth 2.0 Client ID, defaults to environment variable OAUTH2_CLIENT_ID") @@ -312,6 +338,323 @@ and success, unless if the --no-shutdown flag is provided.`, cmd.Flags().String("auth-url", "", "Usually it is enough to specify the `endpoint` flag, but if you want to force the authorization url, use this flag") cmd.Flags().String("token-url", "", "Usually it is enough to specify the `endpoint` flag, but if you want to force the token url, use this flag") cmd.Flags().Bool("https", false, "Sets up HTTPS for the endpoint using a self-signed certificate which is re-generated every time you start this command") + cmd.Flags().Bool("skip", false, "Skip login and/or consent steps if possible. Only effective if you have configured the Login and Consent UI URLs to point to this server.") + cmd.Flags().String("response-mode", "", "Set the response mode. Can be query (default) or form_post.") return cmd } + +type router struct { + cl *openapi.APIClient + skip bool + cmd *cobra.Command + state *string + conf *oauth2.Config + onDone func() + serverLocation string + noShutdown bool +} + +func (rt *router) loginGET(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + req, raw, err := rt.cl.OAuth2API.GetOAuth2LoginRequest(r.Context()). + LoginChallenge(r.URL.Query().Get("login_challenge")). + Execute() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer raw.Body.Close() // to satisfy linter + + if rt.skip && req.GetSkip() { + req, res, err := rt.cl.OAuth2API.AcceptOAuth2LoginRequest(r.Context()). + LoginChallenge(req.Challenge). + AcceptOAuth2LoginRequest(openapi.AcceptOAuth2LoginRequest{Subject: req.Subject}). + Execute() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer res.Body.Close() // to satisfy linter + http.Redirect(w, r, req.RedirectTo, http.StatusFound) + return + } + + pretty, err := prettyJSON(raw.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _ = tokenUserLogin.Execute(w, struct { + LoginChallenge string + Skip bool + SessionID string + Raw string + }{ + LoginChallenge: req.Challenge, + Skip: req.GetSkip(), + SessionID: req.GetSessionId(), + Raw: pretty, + }) +} + +func (rt *router) loginPOST(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if r.FormValue("revoke-consents") == "on" { + res, err := rt.cl.OAuth2API.RevokeOAuth2ConsentSessions(r.Context()). + Subject(r.FormValue("username")). + All(true). + Execute() + if err != nil { + fmt.Fprintln(rt.cmd.ErrOrStderr(), "Error revoking previous consents:", err) + } else { + fmt.Fprintln(rt.cmd.ErrOrStderr(), "Revoked all previous consents") + } + defer res.Body.Close() // to satisfy linter + } + switch r.FormValue("action") { + case "accept": + + req, res, err := rt.cl.OAuth2API.AcceptOAuth2LoginRequest(r.Context()). + LoginChallenge(r.FormValue("ls")). + AcceptOAuth2LoginRequest(openapi.AcceptOAuth2LoginRequest{ + Subject: r.FormValue("username"), + Remember: pointerx.Ptr(r.FormValue("remember") == "on"), + RememberFor: pointerx.Int64(3600), + Context: map[string]string{ + "context from": "login step", + }, + }).Execute() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer res.Body.Close() // to satisfy linter + http.Redirect(w, r, req.RedirectTo, http.StatusFound) + + case "deny": + req, res, err := rt.cl.OAuth2API.RejectOAuth2LoginRequest(r.Context()).LoginChallenge(r.FormValue("ls")).Execute() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer res.Body.Close() // to satisfy linter + http.Redirect(w, r, req.RedirectTo, http.StatusFound) + + default: + http.Error(w, "Invalid action", http.StatusBadRequest) + } +} + +func (rt *router) consentGET(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + req, raw, err := rt.cl.OAuth2API.GetOAuth2ConsentRequest(r.Context()). + ConsentChallenge(r.URL.Query().Get("consent_challenge")). + Execute() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer raw.Body.Close() // to satisfy linter + + if rt.skip && req.GetSkip() { + req, res, err := rt.cl.OAuth2API.AcceptOAuth2ConsentRequest(r.Context()). + ConsentChallenge(req.Challenge). + AcceptOAuth2ConsentRequest(openapi.AcceptOAuth2ConsentRequest{ + GrantScope: req.GetRequestedScope(), + GrantAccessTokenAudience: req.GetRequestedAccessTokenAudience(), + Remember: pointerx.Ptr(true), + RememberFor: pointerx.Int64(3600), + Session: &openapi.AcceptOAuth2ConsentRequestSession{ + AccessToken: map[string]string{ + "foo": "bar", + }, + IdToken: map[string]string{ + "baz": "bar", + }, + }, + }).Execute() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer res.Body.Close() // to satisfy linter + http.Redirect(w, r, req.RedirectTo, http.StatusFound) + return + } + + pretty, err := prettyJSON(raw.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + _, raw, err = rt.cl.OAuth2API.ListOAuth2ConsentSessions(r.Context()). + Subject(req.GetSubject()). + LoginSessionId(req.GetLoginSessionId()). + Execute() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer raw.Body.Close() // to satisfy linter + prettyPrevConsent, err := prettyJSON(raw.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _ = tokenUserConsent.Execute(w, struct { + ConsentChallenge string + Audiences []string + Scopes []string + Skip bool + SessionID string + PreviousConsents string + Raw string + }{ + ConsentChallenge: req.Challenge, + Audiences: req.RequestedAccessTokenAudience, + Scopes: req.RequestedScope, + Skip: req.GetSkip(), + SessionID: req.GetLoginSessionId(), + PreviousConsents: prettyPrevConsent, + Raw: pretty, + }) +} + +func (rt *router) consentPOST(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + switch r.FormValue("action") { + case "accept": + req, res, err := rt.cl.OAuth2API.AcceptOAuth2ConsentRequest(r.Context()). + ConsentChallenge(r.FormValue("cs")). + AcceptOAuth2ConsentRequest(openapi.AcceptOAuth2ConsentRequest{ + GrantScope: r.Form["scope"], + GrantAccessTokenAudience: r.Form["audience"], + Remember: pointerx.Ptr(r.FormValue("remember") == "on"), + RememberFor: pointerx.Int64(3600), + Session: &openapi.AcceptOAuth2ConsentRequestSession{ + AccessToken: map[string]string{ + "foo": "bar", + }, + IdToken: map[string]string{ + "baz": "bar", + }, + }, + }).Execute() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer res.Body.Close() // to satisfy linter + http.Redirect(w, r, req.RedirectTo, http.StatusFound) + + case "deny": + req, res, err := rt.cl.OAuth2API.RejectOAuth2ConsentRequest(r.Context()). + ConsentChallenge(r.FormValue("cs")). + Execute() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer res.Body.Close() // to satisfy linter + http.Redirect(w, r, req.RedirectTo, http.StatusFound) + + default: + http.Error(w, "Invalid action", http.StatusBadRequest) + } +} + +func (rt *router) callback(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + defer rt.onDone() + + if len(r.URL.Query().Get("error")) > 0 { + _, _ = fmt.Fprintf(rt.cmd.ErrOrStderr(), "Got error: %s\n", r.URL.Query().Get("error_description")) + + w.WriteHeader(http.StatusInternalServerError) + _ = tokenUserError.Execute(w, &ed{ + Name: r.URL.Query().Get("error"), + Description: r.URL.Query().Get("error_description"), + Hint: r.URL.Query().Get("error_hint"), + Debug: r.URL.Query().Get("error_debug"), + }) + return + } + + if r.URL.Query().Get("state") != *rt.state { + descr := fmt.Sprintf("States do not match. Expected %q, got %q.", *rt.state, r.URL.Query().Get("state")) + _, _ = fmt.Fprintln(rt.cmd.ErrOrStderr(), descr) + + w.WriteHeader(http.StatusInternalServerError) + _ = tokenUserError.Execute(w, &ed{ + Name: "States do not match", + Description: descr, + }) + return + } + + code := r.URL.Query().Get("code") + ctx := context.WithValue(rt.cmd.Context(), oauth2.HTTPClient, rt.cl) + token, err := rt.conf.Exchange(ctx, code) + if err != nil { + _, _ = fmt.Fprintf(rt.cmd.ErrOrStderr(), "Unable to exchange code for token: %s\n", err) + + w.WriteHeader(http.StatusInternalServerError) + _ = tokenUserError.Execute(w, &ed{ + Name: err.Error(), + }) + return + } + + cmdx.PrintRow(rt.cmd, outputOAuth2Token(*token)) + _ = tokenUserResult.Execute(w, struct { + AccessToken string + RefreshToken string + Expiry string + IDToken string + BackURL string + DisplayBackButton bool + }{ + AccessToken: token.AccessToken, + RefreshToken: token.RefreshToken, + Expiry: token.Expiry.Format(time.RFC1123), + IDToken: fmt.Sprintf("%s", token.Extra("id_token")), + BackURL: rt.serverLocation, + DisplayBackButton: rt.noShutdown, + }) +} + +func (rt *router) callbackPOSTForm(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + u := url.URL{ + Path: r.URL.Path, + RawQuery: r.PostForm.Encode(), + } + http.Redirect(w, r, u.String(), http.StatusFound) +} + +type ed struct { + Name string + Description string + Hint string + Debug string +} + +func prettyJSON(r io.Reader) (string, error) { + contentsRaw, err := io.ReadAll(r) + if err != nil { + return "", err + } + var buf bytes.Buffer + if err := json.Indent(&buf, contentsRaw, "", " "); err != nil { + return "", err + } + return buf.String(), nil +} diff --git a/cmd/cmd_update_client.go b/cmd/cmd_update_client.go index 19c6a096da0..6205b21ad71 100644 --- a/cmd/cmd_update_client.go +++ b/cmd/cmd_update_client.go @@ -42,7 +42,10 @@ To encrypt an auto-generated OAuth2 Client Secret, use flags ` + "`--pgp-key`" + } id := args[0] - cc := clientFromFlags(cmd) + cc, err := clientFromFlags(cmd) + if err != nil { + return err + } client, _, err := m.OAuth2API.SetOAuth2Client(context.Background(), id).OAuth2Client(cc).Execute() //nolint:bodyclose if err != nil { @@ -50,7 +53,7 @@ To encrypt an auto-generated OAuth2 Client Secret, use flags ` + "`--pgp-key`" + } if client.ClientSecret == nil && len(secret) > 0 { - client.ClientSecret = pointerx.String(secret) + client.ClientSecret = pointerx.Ptr(secret) } if encryptSecret && client.ClientSecret != nil { @@ -60,7 +63,7 @@ To encrypt an auto-generated OAuth2 Client Secret, use flags ` + "`--pgp-key`" + return cmdx.FailSilently(cmd) } - client.ClientSecret = pointerx.String(enc.Base64Encode()) + client.ClientSecret = pointerx.Ptr(enc.Base64Encode()) } cmdx.PrintRow(cmd, (*outputOAuth2Client)(client)) diff --git a/cmd/cmd_update_client_test.go b/cmd/cmd_update_client_test.go index c21aa0277bc..6cbcb7dfe5f 100644 --- a/cmd/cmd_update_client_test.go +++ b/cmd/cmd_update_client_test.go @@ -4,10 +4,13 @@ package cmd_test import ( + "bytes" "context" "encoding/json" "testing" + "github.com/tidwall/sjson" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" @@ -25,11 +28,11 @@ func TestUpdateClient(t *testing.T) { original := createClient(t, reg, nil) t.Run("case=creates successfully", func(t *testing.T) { actual := gjson.Parse(cmdx.ExecNoErr(t, c, "--grant-type", "implicit", original.GetID())) - expected, err := reg.ClientManager().GetClient(ctx, actual.Get("client_id").String()) + expected, err := reg.ClientManager().GetClient(ctx, actual.Get("client_id").Str) require.NoError(t, err) - assert.Equal(t, expected.GetID(), actual.Get("client_id").String()) - assert.Equal(t, "implicit", actual.Get("grant_types").Array()[0].String()) + assert.Equal(t, expected.GetID(), actual.Get("client_id").Str) + assert.Equal(t, "implicit", actual.Get("grant_types").Array()[0].Str) snapshotx.SnapshotT(t, json.RawMessage(actual.Raw), snapshotExcludedClientFields...) }) @@ -39,9 +42,48 @@ func TestUpdateClient(t *testing.T) { "--secret", "some-userset-secret", "--pgp-key", base64EncodedPGPPublicKey(t), )) - assert.NotEmpty(t, actual.Get("client_id").String()) - assert.NotEmpty(t, actual.Get("client_secret").String()) + assert.Equal(t, original.ID, actual.Get("client_id").Str) + assert.NotEmpty(t, actual.Get("client_secret").Str) + assert.NotEqual(t, original.Secret, actual.Get("client_secret").Str) snapshotx.SnapshotT(t, json.RawMessage(actual.Raw), snapshotExcludedClientFields...) }) + + t.Run("case=updates from file", func(t *testing.T) { + original, err := reg.ClientManager().GetConcreteClient(ctx, original.GetID()) + require.NoError(t, err) + + raw, err := json.Marshal(original) + require.NoError(t, err) + + t.Run("file=stdin", func(t *testing.T) { + raw, err = sjson.SetBytes(raw, "client_name", "updated through file stdin") + require.NoError(t, err) + + stdout, stderr, err := cmdx.Exec(t, c, bytes.NewReader(raw), original.GetID(), "--file", "-") + require.NoError(t, err, stderr) + + actual := gjson.Parse(stdout) + assert.Equal(t, original.ID, actual.Get("client_id").Str) + assert.Equal(t, "updated through file stdin", actual.Get("client_name").Str) + + snapshotx.SnapshotT(t, json.RawMessage(actual.Raw), snapshotExcludedClientFields...) + }) + + t.Run("file=from disk", func(t *testing.T) { + raw, err = sjson.SetBytes(raw, "client_name", "updated through file from disk") + require.NoError(t, err) + + fn := writeTempFile(t, json.RawMessage(raw)) + + stdout, stderr, err := cmdx.Exec(t, c, nil, original.GetID(), "--file", fn) + require.NoError(t, err, stderr) + + actual := gjson.Parse(stdout) + assert.Equal(t, original.ID, actual.Get("client_id").Str) + assert.Equal(t, "updated through file from disk", actual.Get("client_name").Str) + + snapshotx.SnapshotT(t, json.RawMessage(actual.Raw), snapshotExcludedClientFields...) + }) + }) } diff --git a/cmd/janitor.go b/cmd/janitor.go index 56c2d006486..be3d7f9f33d 100644 --- a/cmd/janitor.go +++ b/cmd/janitor.go @@ -15,9 +15,9 @@ import ( func NewJanitorCmd(slOpts []servicelocatorx.Option, dOpts []driver.OptionsModifier, cOpts []configx.OptionModifier) *cobra.Command { cmd := &cobra.Command{ - Use: "janitor []", + Use: "janitor [[database_url]]", Short: "This command cleans up stale database rows.", - Example: `hydra janitor --keep-if-younger 23h --access-lifespan 1h --refresh-lifespan 40h --consent-request-lifespan 10m `, + Example: `hydra janitor --keep-if-younger 23h --access-lifespan 1h --refresh-lifespan 40h --consent-request-lifespan 10m [database_url]`, Long: `This command cleans up stale database rows. This will select records to delete with a limit and delete records in batch to ensure that no table locking issues arise in big production databases. diff --git a/cmd/migrate_sql.go b/cmd/migrate_sql.go index 1ab3bde3717..50b687d4470 100644 --- a/cmd/migrate_sql.go +++ b/cmd/migrate_sql.go @@ -6,17 +6,19 @@ package cmd import ( "github.com/spf13/cobra" + "github.com/ory/x/popx" + + "github.com/ory/hydra/v2/cmd/cli" "github.com/ory/hydra/v2/driver" "github.com/ory/x/configx" "github.com/ory/x/servicelocatorx" - - "github.com/ory/hydra/v2/cmd/cli" ) -func NewMigrateSqlCmd(slOpts []servicelocatorx.Option, dOpts []driver.OptionsModifier, cOpts []configx.OptionModifier) *cobra.Command { +func NewMigrateSQLCmd(slOpts []servicelocatorx.Option, dOpts []driver.OptionsModifier, cOpts []configx.OptionModifier) *cobra.Command { cmd := &cobra.Command{ - Use: "sql ", - Short: "Create SQL schemas and apply migration plans", + Use: "sql [database_url]", + Deprecated: "Please use `hydra migrate sql up` instead.", + Short: "Perform SQL migrations", Long: `Run this command on a fresh SQL installation and when you upgrade Hydra to a new minor version. For example, upgrading Hydra 0.7.0 to 0.8.0 requires running this command. @@ -25,16 +27,32 @@ This decreases risk of failure and decreases time required. You can read in the database URL using the -e flag, for example: export DSN=... - hydra migrate sql -e + hydra migrate sql up -e ### WARNING ### Before running this command on an existing database, create a back up!`, - RunE: cli.NewHandler(slOpts, dOpts, cOpts).Migration.MigrateSQL, + RunE: cli.NewHandler(slOpts, dOpts, cOpts).Migration.MigrateSQLUp, } - cmd.Flags().BoolP("read-from-env", "e", false, "If set, reads the database connection string from the environment variable DSN or config file key dsn.") cmd.Flags().BoolP("yes", "y", false, "If set all confirmation requests are accepted without user interaction.") + cmd.PersistentFlags().BoolP("read-from-env", "e", false, "If set, reads the database connection string from the environment variable DSN or config file key dsn.") + + cmd.AddCommand(NewMigrateSQLDownCmd(slOpts, dOpts, cOpts)) + cmd.AddCommand(NewMigrateSQLUpCmd(slOpts, dOpts, cOpts)) + cmd.AddCommand(NewMigrateSQLStatusCmd(slOpts, dOpts, cOpts)) return cmd } + +func NewMigrateSQLDownCmd(slOpts []servicelocatorx.Option, dOpts []driver.OptionsModifier, cOpts []configx.OptionModifier) *cobra.Command { + return popx.NewMigrateSQLDownCmd("hydra", cli.NewHandler(slOpts, dOpts, cOpts).Migration.MigrateSQLDown) +} + +func NewMigrateSQLStatusCmd(slOpts []servicelocatorx.Option, dOpts []driver.OptionsModifier, cOpts []configx.OptionModifier) *cobra.Command { + return popx.NewMigrateSQLStatusCmd("hydra", cli.NewHandler(slOpts, dOpts, cOpts).Migration.MigrateStatus) +} + +func NewMigrateSQLUpCmd(slOpts []servicelocatorx.Option, dOpts []driver.OptionsModifier, cOpts []configx.OptionModifier) *cobra.Command { + return popx.NewMigrateSQLUpCmd("hydra", cli.NewHandler(slOpts, dOpts, cOpts).Migration.MigrateSQLUp) +} diff --git a/cmd/migrate_status.go b/cmd/migrate_status.go index 940728b79b6..b1529d9c48c 100644 --- a/cmd/migrate_status.go +++ b/cmd/migrate_status.go @@ -4,8 +4,8 @@ package cmd import ( - "github.com/ory/x/cmdx" "github.com/ory/x/configx" + "github.com/ory/x/popx" "github.com/ory/x/servicelocatorx" "github.com/spf13/cobra" @@ -15,15 +15,12 @@ import ( ) func NewMigrateStatusCmd(slOpts []servicelocatorx.Option, dOpts []driver.OptionsModifier, cOpts []configx.OptionModifier) *cobra.Command { - cmd := &cobra.Command{ - Use: "status", - Short: "Get the current migration status", - RunE: cli.NewHandler(slOpts, dOpts, cOpts).Migration.MigrateStatus, - } - - cmdx.RegisterFormatFlags(cmd.PersistentFlags()) - cmd.Flags().BoolP("read-from-env", "e", false, "If set, reads the database connection string from the environment variable DSN or config file key dsn.") - cmd.Flags().Bool("block", false, "Block until all migrations have been applied") - + cmd := popx.RegisterMigrateStatusFlags(&cobra.Command{ + Use: "status", + Deprecated: "Please use `hydra migrate sql status` instead.", + Short: "Get the current migration status", + RunE: cli.NewHandler(slOpts, dOpts, cOpts).Migration.MigrateStatus, + }) + cmd.PersistentFlags().BoolP("read-from-env", "e", false, "If set, reads the database connection string from the environment variable DSN or config file key dsn.") return cmd } diff --git a/cmd/root.go b/cmd/root.go index 6feabdb8103..20832d40546 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,6 +7,8 @@ import ( "fmt" "os" + "github.com/pkg/errors" + "github.com/ory/x/cmdx" "github.com/ory/hydra/v2/driver" @@ -72,7 +74,7 @@ func RegisterCommandRecursive(parent *cobra.Command, slOpts []servicelocatorx.Op migrateCmd := NewMigrateCmd() migrateCmd.AddCommand(NewMigrateGenCmd()) - migrateCmd.AddCommand(NewMigrateSqlCmd(slOpts, dOpts, cOpts)) + migrateCmd.AddCommand(NewMigrateSQLCmd(slOpts, dOpts, cOpts)) migrateCmd.AddCommand(NewMigrateStatusCmd(slOpts, dOpts, cOpts)) serveCmd := NewServeCmd() @@ -99,8 +101,11 @@ func RegisterCommandRecursive(parent *cobra.Command, slOpts []servicelocatorx.Op // Execute adds all child commands to the root command sets flags appropriately. func Execute() { - if err := NewRootCmd(nil, nil, nil).Execute(); err != nil { - fmt.Println(err) - os.Exit(-1) + c := NewRootCmd(nil, nil, nil) + if err := c.Execute(); err != nil { + if !errors.Is(err, cmdx.ErrNoPrintButFail) { + _, _ = fmt.Fprintln(c.ErrOrStderr(), err) + } + os.Exit(1) } } diff --git a/consent/handler.go b/consent/handler.go index 2eea50bc8d8..48167e7380f 100644 --- a/consent/handler.go +++ b/consent/handler.go @@ -4,6 +4,7 @@ package consent import ( + "context" "encoding/json" "net/http" "net/url" @@ -476,11 +477,12 @@ func (h *Handler) acceptOAuth2LoginRequest(w http.ResponseWriter, r *http.Reques } handledLoginRequest.RequestedAt = loginRequest.RequestedAt - f, err := flowctx.Decode[flow.Flow](ctx, h.r.FlowCipher(), challenge, flowctx.AsLoginChallenge) + f, err := h.decodeFlowWithClient(ctx, challenge, flowctx.AsLoginChallenge) if err != nil { h.r.Writer().WriteError(w, r, err) return } + request, err := h.r.ConsentManager().HandleLoginRequest(ctx, f, challenge, &handledLoginRequest) if err != nil { h.r.Writer().WriteError(w, r, errorsx.WithStack(err)) @@ -575,7 +577,7 @@ func (h *Handler) rejectOAuth2LoginRequest(w http.ResponseWriter, r *http.Reques return } - f, err := flowctx.Decode[flow.Flow](ctx, h.r.FlowCipher(), challenge, flowctx.AsLoginChallenge) + f, err := h.decodeFlowWithClient(ctx, challenge, flowctx.AsLoginChallenge) if err != nil { h.r.Writer().WriteError(w, r, err) return @@ -761,7 +763,7 @@ func (h *Handler) acceptOAuth2ConsentRequest(w http.ResponseWriter, r *http.Requ p.RequestedAt = cr.RequestedAt p.HandledAt = sqlxx.NullTime(time.Now().UTC()) - f, err := flowctx.Decode[flow.Flow](ctx, h.r.FlowCipher(), challenge, flowctx.AsConsentChallenge) + f, err := h.decodeFlowWithClient(ctx, challenge, flowctx.AsConsentChallenge) if err != nil { h.r.Writer().WriteError(w, r, err) return @@ -868,7 +870,7 @@ func (h *Handler) rejectOAuth2ConsentRequest(w http.ResponseWriter, r *http.Requ return } - f, err := flowctx.Decode[flow.Flow](ctx, h.r.FlowCipher(), challenge, flowctx.AsConsentChallenge) + f, err := h.decodeFlowWithClient(ctx, challenge, flowctx.AsConsentChallenge) if err != nil { h.r.Writer().WriteError(w, r, err) return @@ -1044,3 +1046,12 @@ func (h *Handler) getOAuth2LogoutRequest(w http.ResponseWriter, r *http.Request, h.r.Writer().Write(w, r, request) } + +func (h *Handler) decodeFlowWithClient(ctx context.Context, challenge string, opts ...flowctx.CodecOption) (*flow.Flow, error) { + f, err := flowctx.Decode[flow.Flow](ctx, h.r.FlowCipher(), challenge, opts...) + if err != nil { + return nil, err + } + + return f, nil +} diff --git a/consent/strategy_default.go b/consent/strategy_default.go index 395742fc47c..8acf7f7c2fa 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -7,6 +7,7 @@ import ( "context" stderrs "errors" "fmt" + "io" "net/http" "net/url" "strconv" @@ -790,6 +791,7 @@ func (s *DefaultStrategy) executeBackChannelLogout(r *http.Request, subject, sid return } defer res.Body.Close() + res.Body = io.NopCloser(io.LimitReader(res.Body, 1<<20 /* 1 MB */)) // in case we ever start to read this response if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNoContent { log.WithError(errors.Errorf("expected HTTP status code %d or %d but got %d", http.StatusOK, http.StatusNoContent, res.StatusCode)). @@ -1170,13 +1172,3 @@ func (s *DefaultStrategy) ObfuscateSubjectIdentifier(ctx context.Context, cl fos } return subject, nil } - -func (s *DefaultStrategy) loginSessionFromCookie(r *http.Request) *flow.LoginSession { - clientID := r.URL.Query().Get("client_id") - if clientID == "" { - return nil - } - ls, _ := flowctx.FromCookie[flow.LoginSession](r.Context(), r, s.r.FlowCipher(), flowctx.LoginSessionCookie(flowctx.SuffixFromStatic(clientID))) - - return ls -} diff --git a/consent/strategy_oauth_test.go b/consent/strategy_oauth_test.go index f1827fffd50..370a3378074 100644 --- a/consent/strategy_oauth_test.go +++ b/consent/strategy_oauth_test.go @@ -823,7 +823,7 @@ func TestStrategyLoginConsentNext(t *testing.T) { makeRequestAndExpectCode(t, hc, c, url.Values{}) // Make request with additional scope and prompt none, which fails - makeRequestAndExpectError(t, hc, c, url.Values{"prompt": {"none"}, "scope": {"openid"}}, + makeRequestAndExpectError(t, hc, c, url.Values{"prompt": {"none"}, "scope": {"openid"}, "redirect_uri": {c.RedirectURIs[0]}}, "Prompt 'none' was requested, but no previous consent was found") }) @@ -930,11 +930,11 @@ func TestStrategyLoginConsentNext(t *testing.T) { }{ { d: "check all the sub claims", - values: url.Values{"scope": {"openid"}}, + values: url.Values{"scope": {"openid"}, "redirect_uri": {c.RedirectURIs[0]}}, }, { d: "works with id_token_hint", - values: url.Values{"scope": {"openid"}, "id_token_hint": {testhelpers.NewIDToken(t, reg, hash)}}, + values: url.Values{"scope": {"openid"}, "redirect_uri": {c.RedirectURIs[0]}, "id_token_hint": {testhelpers.NewIDToken(t, reg, hash)}}, }, } { t.Run("case="+tc.d, func(t *testing.T) { @@ -974,7 +974,7 @@ func TestStrategyLoginConsentNext(t *testing.T) { }), acceptConsentHandler(t, &hydra.AcceptOAuth2ConsentRequest{GrantScope: []string{"openid"}})) - code := makeRequestAndExpectCode(t, nil, c, url.Values{}) + code := makeRequestAndExpectCode(t, nil, c, url.Values{"redirect_uri": {c.RedirectURIs[0]}}) conf := oauth2Config(t, c) token, err := conf.Exchange(context.Background(), code) diff --git a/driver/config/provider.go b/driver/config/provider.go index ba1869498fe..52b9ee45a3f 100644 --- a/driver/config/provider.go +++ b/driver/config/provider.go @@ -6,6 +6,7 @@ package config import ( "context" "fmt" + "math" "net/http" "net/url" "strings" @@ -101,6 +102,7 @@ const ( KeyExcludeNotBeforeClaim = "oauth2.exclude_not_before_claim" KeyAllowedTopLevelClaims = "oauth2.allowed_top_level_claims" KeyMirrorTopLevelClaims = "oauth2.mirror_top_level_claims" + KeyRefreshTokenRotationGracePeriod = "oauth2.grant.refresh_token.rotation_grace_period" // #nosec G101 KeyOAuth2GrantJWTIDOptional = "oauth2.grant.jwt.jti_optional" KeyOAuth2GrantJWTIssuedDateOptional = "oauth2.grant.jwt.iat_optional" KeyOAuth2GrantJWTMaxDuration = "oauth2.grant.jwt.max_ttl" @@ -134,15 +136,34 @@ func (p *DefaultProvider) GetHasherAlgorithm(ctx context.Context) x.HashAlgorith } func (p *DefaultProvider) HasherBcryptConfig(ctx context.Context) *hasherx.BCryptConfig { + var cost uint32 + costInt := int64(p.GetBCryptCost(ctx)) + if costInt < 0 { + cost = 10 + } else if costInt > math.MaxUint32 { + cost = math.MaxUint32 + } else { + cost = uint32(costInt) + } return &hasherx.BCryptConfig{ - Cost: uint32(p.GetBCryptCost(ctx)), + Cost: cost, } } func (p *DefaultProvider) HasherPBKDF2Config(ctx context.Context) *hasherx.PBKDF2Config { + var iters uint32 + itersInt := p.getProvider(ctx).Int64(KeyPBKDF2Iterations) + if itersInt < 1 { + iters = 1 + } else if int64(itersInt) > math.MaxUint32 { + iters = math.MaxUint32 + } else { + iters = uint32(itersInt) + } + return &hasherx.PBKDF2Config{ Algorithm: "sha256", - Iterations: uint32(p.getProvider(ctx).Int(KeyPBKDF2Iterations)), + Iterations: iters, SaltLength: 16, KeyLength: 32, } @@ -649,3 +670,11 @@ func (p *DefaultProvider) cookieSuffix(ctx context.Context, key string) string { return p.getProvider(ctx).String(key) + suffix } + +func (p *DefaultProvider) RefreshTokenRotationGracePeriod(ctx context.Context) time.Duration { + gracePeriod := p.getProvider(ctx).DurationF(KeyRefreshTokenRotationGracePeriod, 0) + if gracePeriod > time.Hour { + return time.Hour + } + return gracePeriod +} diff --git a/driver/config/provider_test.go b/driver/config/provider_test.go index 8e5c44a9e2e..168ca81d69f 100644 --- a/driver/config/provider_test.go +++ b/driver/config/provider_test.go @@ -291,6 +291,13 @@ func TestViperProviderValidates(t *testing.T) { assert.Equal(t, "random_salt", c.SubjectIdentifierAlgorithmSalt(ctx)) assert.Equal(t, []string{"whatever"}, c.DefaultClientScope(ctx)) + // refresh + assert.Equal(t, time.Duration(0), c.RefreshTokenRotationGracePeriod(ctx)) + require.NoError(t, c.Set(ctx, KeyRefreshTokenRotationGracePeriod, "1s")) + assert.Equal(t, time.Second, c.RefreshTokenRotationGracePeriod(ctx)) + require.NoError(t, c.Set(ctx, KeyRefreshTokenRotationGracePeriod, "2h")) + assert.Equal(t, time.Hour, c.RefreshTokenRotationGracePeriod(ctx)) + // urls assert.Equal(t, urlx.ParseOrPanic("https://issuer"), c.IssuerURL(ctx)) assert.Equal(t, urlx.ParseOrPanic("https://public/"), c.PublicURL(ctx)) diff --git a/driver/config/serve.go b/driver/config/serve.go index f37dcde41eb..21932e3078f 100644 --- a/driver/config/serve.go +++ b/driver/config/serve.go @@ -6,7 +6,8 @@ package config import ( "context" "fmt" - "os" + "io/fs" + "math" "strings" "github.com/ory/x/contextx" @@ -63,10 +64,22 @@ func (p *DefaultProvider) ListenOn(iface ServeInterface) string { } func (p *DefaultProvider) SocketPermission(iface ServeInterface) *configx.UnixPermission { + modeInt := int64(0o755) + if p.getProvider(contextx.RootContext).Exists(iface.Key(KeySuffixSocketMode)) { + modeInt = int64(p.getProvider(contextx.RootContext).Int(iface.Key(KeySuffixSocketMode))) + } + mode := fs.FileMode(0) + if modeInt < 0 { + mode = 0 + } else if modeInt > math.MaxUint32 { + mode = 0777 + } else { + mode = fs.FileMode(modeInt) + } return &configx.UnixPermission{ Owner: p.getProvider(contextx.RootContext).String(iface.Key(KeySuffixSocketOwner)), Group: p.getProvider(contextx.RootContext).String(iface.Key(KeySuffixSocketGroup)), - Mode: os.FileMode(p.getProvider(contextx.RootContext).IntF(iface.Key(KeySuffixSocketMode), 0755)), + Mode: mode, } } diff --git a/driver/registry_sql.go b/driver/registry_sql.go index 76159e4c805..7cef650f955 100644 --- a/driver/registry_sql.go +++ b/driver/registry_sql.go @@ -247,8 +247,12 @@ func (m *RegistrySQL) alwaysCanHandle(dsn string) bool { return s == dbal.DriverMySQL || s == dbal.DriverPostgreSQL || s == dbal.DriverCockroachDB } +func (m *RegistrySQL) PingContext(ctx context.Context) error { + return m.Persister().Ping(ctx) +} + func (m *RegistrySQL) Ping() error { - return m.Persister().Ping() + return m.PingContext(context.Background()) } func (m *RegistrySQL) ClientManager() client.Manager { @@ -439,8 +443,8 @@ func (m *RegistrySQL) GrantValidator() *trust.GrantValidator { func (m *RegistrySQL) HealthHandler() *healthx.Handler { if m.hh == nil { m.hh = healthx.NewHandler(m.Writer(), m.buildVersion, healthx.ReadyCheckers{ - "database": func(_ *http.Request) error { - return m.Ping() + "database": func(r *http.Request) error { + return m.PingContext(r.Context()) }, "migrations": func(r *http.Request) error { if m.migrationStatus != nil && !m.migrationStatus.HasPending() { diff --git a/flow/flow.go b/flow/flow.go index b926ce5db96..0502bc544ba 100644 --- a/flow/flow.go +++ b/flow/flow.go @@ -84,18 +84,18 @@ type Flow struct { // identify the session. // // required: true - ID string `db:"login_challenge"` - NID uuid.UUID `db:"nid"` + ID string `db:"login_challenge" json:"i"` + NID uuid.UUID `db:"nid" json:"n"` // RequestedScope contains the OAuth 2.0 Scope requested by the OAuth 2.0 Client. // // required: true - RequestedScope sqlxx.StringSliceJSONFormat `db:"requested_scope"` + RequestedScope sqlxx.StringSliceJSONFormat `db:"requested_scope" json:"rs,omitempty"` // RequestedAudience contains the access token audience as requested by the OAuth 2.0 Client. // // required: true - RequestedAudience sqlxx.StringSliceJSONFormat `db:"requested_at_audience"` + RequestedAudience sqlxx.StringSliceJSONFormat `db:"requested_at_audience" json:"ra,omitempty"` // LoginSkip, if true, implies that the client has requested the same scopes from the same user previously. // If true, you can skip asking the user to grant the requested scopes, and simply forward the user to the redirect URL. @@ -103,73 +103,72 @@ type Flow struct { // This feature allows you to update / set session information. // // required: true - LoginSkip bool `db:"login_skip"` + LoginSkip bool `db:"login_skip" json:"ls,omitempty"` // Subject is the user ID of the end-user that authenticated. Now, that end user needs to grant or deny the scope // requested by the OAuth 2.0 client. If this value is set and `skip` is true, you MUST include this subject type // when accepting the login request, or the request will fail. // // required: true - Subject string `db:"subject"` + Subject string `db:"subject" json:"s,omitempty"` // OpenIDConnectContext provides context for the (potential) OpenID Connect context. Implementation of these // values in your app are optional but can be useful if you want to be fully compliant with the OpenID Connect spec. - OpenIDConnectContext *OAuth2ConsentRequestOpenIDConnectContext `db:"oidc_context"` + OpenIDConnectContext *OAuth2ConsentRequestOpenIDConnectContext `db:"oidc_context" json:"oc"` // Client is the OAuth 2.0 Client that initiated the request. // // required: true - Client *client.Client `db:"-"` - - ClientID string `db:"client_id"` + Client *client.Client `db:"-" json:"c,omitempty"` + ClientID string `db:"client_id" json:"ci,omitempty"` // RequestURL is the original OAuth 2.0 Authorization URL requested by the OAuth 2.0 client. It is the URL which // initiates the OAuth 2.0 Authorization Code or OAuth 2.0 Implicit flow. This URL is typically not needed, but // might come in handy if you want to deal with additional request parameters. // // required: true - RequestURL string `db:"request_url"` + RequestURL string `db:"request_url" json:"r,omitempty"` // SessionID is the login session ID. If the user-agent reuses a login session (via cookie / remember flag) // this ID will remain the same. If the user-agent did not have an existing authentication session (e.g. remember is false) // this will be a new random value. This value is used as the "sid" parameter in the ID Token and in OIDC Front-/Back- // channel logout. Its value can generally be used to associate consecutive login requests by a certain user. - SessionID sqlxx.NullString `db:"login_session_id"` + SessionID sqlxx.NullString `db:"login_session_id" json:"si,omitempty"` // IdentityProviderSessionID is the session ID of the end-user that authenticated. // If specified, we will use this value to propagate the logout. - IdentityProviderSessionID sqlxx.NullString `db:"identity_provider_session_id"` + IdentityProviderSessionID sqlxx.NullString `db:"identity_provider_session_id" json:"is,omitempty"` - LoginVerifier string `db:"login_verifier"` - LoginCSRF string `db:"login_csrf"` + LoginVerifier string `db:"login_verifier" json:"lv,omitempty"` + LoginCSRF string `db:"login_csrf" json:"lc,omitempty"` - LoginInitializedAt sqlxx.NullTime `db:"login_initialized_at"` - RequestedAt time.Time `db:"requested_at"` + LoginInitializedAt sqlxx.NullTime `db:"login_initialized_at" json:"li,omitempty"` + RequestedAt time.Time `db:"requested_at" json:"ia,omitempty"` - State int16 `db:"state"` + State int16 `db:"state" json:"q,omitempty"` // LoginRemember, if set to true, tells ORY Hydra to remember this user by telling the user agent (browser) to store // a cookie with authentication data. If the same user performs another OAuth 2.0 Authorization Request, he/she // will not be asked to log in again. - LoginRemember bool `db:"login_remember"` + LoginRemember bool `db:"login_remember" json:"lr,omitempty"` // LoginRememberFor sets how long the authentication should be remembered for in seconds. If set to `0`, the // authorization will be remembered for the duration of the browser session (using a session cookie). - LoginRememberFor int `db:"login_remember_for"` + LoginRememberFor int `db:"login_remember_for" json:"lf,omitempty"` // LoginExtendSessionLifespan, if set to true, session cookie expiry time will be updated when session is // refreshed (login skip=true). - LoginExtendSessionLifespan bool `db:"login_extend_session_lifespan"` + LoginExtendSessionLifespan bool `db:"login_extend_session_lifespan" json:"ll,omitempty"` // ACR sets the Authentication AuthorizationContext Class Reference value for this authentication session. You can use it // to express that, for example, a user authenticated using two factor authentication. - ACR string `db:"acr"` + ACR string `db:"acr" json:"a,omitempty"` // AMR sets the Authentication Methods References value for this // authentication session. You can use it to specify the method a user used to // authenticate. For example, if the acr indicates a user used two factor // authentication, the amr can express they used a software-secured key. - AMR sqlxx.StringSliceJSONFormat `db:"amr"` + AMR sqlxx.StringSliceJSONFormat `db:"amr" json:"am,omitempty"` // ForceSubjectIdentifier forces the "pairwise" user ID of the end-user that authenticated. The "pairwise" user ID refers to the // (Pairwise Identifier Algorithm)[http://openid.net/specs/openid-connect-core-1_0.html#PairwiseAlg] of the OpenID @@ -188,58 +187,58 @@ type Flow struct { // other unique value). // // If you fail to compute the proper value, then authentication processes which have id_token_hint set might fail. - ForceSubjectIdentifier string `db:"forced_subject_identifier"` + ForceSubjectIdentifier string `db:"forced_subject_identifier" json:"fs,omitempty"` // Context is an optional object which can hold arbitrary data. The data will be made available when fetching the // consent request under the "context" field. This is useful in scenarios where login and consent endpoints share // data. - Context sqlxx.JSONRawMessage `db:"context"` + Context sqlxx.JSONRawMessage `db:"context" json:"ct"` // LoginWasUsed set to true means that the login request was already handled. // This can happen on form double-submit or other errors. If this is set we // recommend redirecting the user to `request_url` to re-initiate the flow. - LoginWasUsed bool `db:"login_was_used"` + LoginWasUsed bool `db:"login_was_used" json:"lu,omitempty"` - LoginError *RequestDeniedError `db:"login_error"` - LoginAuthenticatedAt sqlxx.NullTime `db:"login_authenticated_at"` + LoginError *RequestDeniedError `db:"login_error" json:"le,omitempty"` + LoginAuthenticatedAt sqlxx.NullTime `db:"login_authenticated_at" json:"la,omitempty"` // ConsentChallengeID is the identifier ("authorization challenge") of the consent authorization request. It is used to // identify the session. // // required: true - ConsentChallengeID sqlxx.NullString `db:"consent_challenge_id"` + ConsentChallengeID sqlxx.NullString `db:"consent_challenge_id" json:"cc,omitempty"` // ConsentSkip, if true, implies that the client has requested the same scopes from the same user previously. // If true, you must not ask the user to grant the requested scopes. You must however either allow or deny the // consent request using the usual API call. - ConsentSkip bool `db:"consent_skip"` - ConsentVerifier sqlxx.NullString `db:"consent_verifier"` - ConsentCSRF sqlxx.NullString `db:"consent_csrf"` + ConsentSkip bool `db:"consent_skip" json:"cs,omitempty"` + ConsentVerifier sqlxx.NullString `db:"consent_verifier" json:"cv,omitempty"` + ConsentCSRF sqlxx.NullString `db:"consent_csrf" json:"cr,omitempty"` // GrantedScope sets the scope the user authorized the client to use. Should be a subset of `requested_scope`. - GrantedScope sqlxx.StringSliceJSONFormat `db:"granted_scope"` + GrantedScope sqlxx.StringSliceJSONFormat `db:"granted_scope" json:"gs,omitempty"` // GrantedAudience sets the audience the user authorized the client to use. Should be a subset of `requested_access_token_audience`. - GrantedAudience sqlxx.StringSliceJSONFormat `db:"granted_at_audience"` + GrantedAudience sqlxx.StringSliceJSONFormat `db:"granted_at_audience" json:"ga,omitempty"` // ConsentRemember, if set to true, tells ORY Hydra to remember this consent authorization and reuse it if the same // client asks the same user for the same, or a subset of, scope. - ConsentRemember bool `db:"consent_remember"` + ConsentRemember bool `db:"consent_remember" json:"ce,omitempty"` // ConsentRememberFor sets how long the consent authorization should be remembered for in seconds. If set to `0`, the // authorization will be remembered indefinitely. - ConsentRememberFor *int `db:"consent_remember_for"` + ConsentRememberFor *int `db:"consent_remember_for" json:"cf"` // ConsentHandledAt contains the timestamp the consent request was handled. - ConsentHandledAt sqlxx.NullTime `db:"consent_handled_at"` + ConsentHandledAt sqlxx.NullTime `db:"consent_handled_at" json:"ch,omitempty"` // ConsentWasHandled set to true means that the request was already handled. // This can happen on form double-submit or other errors. If this is set we // recommend redirecting the user to `request_url` to re-initiate the flow. - ConsentWasHandled bool `db:"consent_was_used"` - ConsentError *RequestDeniedError `db:"consent_error"` - SessionIDToken sqlxx.MapStringInterface `db:"session_id_token" faker:"-"` - SessionAccessToken sqlxx.MapStringInterface `db:"session_access_token" faker:"-"` + ConsentWasHandled bool `db:"consent_was_used" json:"cw,omitempty"` + ConsentError *RequestDeniedError `db:"consent_error" json:"cx"` + SessionIDToken sqlxx.MapStringInterface `db:"session_id_token" faker:"-" json:"st"` + SessionAccessToken sqlxx.MapStringInterface `db:"session_access_token" faker:"-" json:"sa"` } func NewFlow(r *LoginRequest) *Flow { @@ -511,21 +510,33 @@ type CipherProvider interface { } // ToLoginChallenge converts the flow into a login challenge. -func (f *Flow) ToLoginChallenge(ctx context.Context, cipherProvider CipherProvider) (string, error) { +func (f Flow) ToLoginChallenge(ctx context.Context, cipherProvider CipherProvider) (string, error) { + if f.Client != nil { + f.ClientID = f.Client.GetID() + } return flowctx.Encode(ctx, cipherProvider.FlowCipher(), f, flowctx.AsLoginChallenge) } // ToLoginVerifier converts the flow into a login verifier. -func (f *Flow) ToLoginVerifier(ctx context.Context, cipherProvider CipherProvider) (string, error) { +func (f Flow) ToLoginVerifier(ctx context.Context, cipherProvider CipherProvider) (string, error) { + if f.Client != nil { + f.ClientID = f.Client.GetID() + } return flowctx.Encode(ctx, cipherProvider.FlowCipher(), f, flowctx.AsLoginVerifier) } // ToConsentChallenge converts the flow into a consent challenge. -func (f *Flow) ToConsentChallenge(ctx context.Context, cipherProvider CipherProvider) (string, error) { +func (f Flow) ToConsentChallenge(ctx context.Context, cipherProvider CipherProvider) (string, error) { + if f.Client != nil { + f.ClientID = f.Client.GetID() + } return flowctx.Encode(ctx, cipherProvider.FlowCipher(), f, flowctx.AsConsentChallenge) } // ToConsentVerifier converts the flow into a consent verifier. -func (f *Flow) ToConsentVerifier(ctx context.Context, cipherProvider CipherProvider) (string, error) { +func (f Flow) ToConsentVerifier(ctx context.Context, cipherProvider CipherProvider) (string, error) { + if f.Client != nil { + f.ClientID = f.Client.GetID() + } return flowctx.Encode(ctx, cipherProvider.FlowCipher(), f, flowctx.AsConsentVerifier) } diff --git a/go.mod b/go.mod index 08033c6b508..bbf5c8b09d9 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,14 @@ toolchain go1.22.5 replace github.com/ory/hydra-client-go/v2 => ./internal/httpclient -replace github.com/gobuffalo/pop/v6 => github.com/ory/pop/v6 v6.2.0 +replace github.com/gobuffalo/pop/v6 => github.com/ory/pop/v6 v6.2.1-0.20241121111754-e5dfc0f3344b + +// Bump Fosite to https://github.com/ory/fosite/tree/hperl/v0.47.0%2B168636f, which contains +// https://github.com/ory/fosite/commit/b40b1cbb1997e2160eaaf97fb6f73960db4c6118 and https://github.com/ory/fosite/pull/833/commits/eab241e153a4c97abe2e4c6e654f20b9ae206473 on top of the latest release. +// +// This is needed until we release the next version of the master branch, as that branch already contains the redirect URI validation fix, which +// may be breaking for some users. +replace github.com/ory/fosite => github.com/ory/fosite v0.47.1-0.20241101073333-eab241e153a4 require ( github.com/ThalesIgnite/crypto11 v1.2.5 @@ -40,7 +47,7 @@ require ( github.com/ory/hydra-client-go/v2 v2.2.1 github.com/ory/jsonschema/v3 v3.0.8 github.com/ory/kratos-client-go v1.2.1 - github.com/ory/x v0.0.647 + github.com/ory/x v0.0.675 github.com/pborman/uuid v1.2.1 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.19.1 @@ -56,21 +63,19 @@ require ( github.com/toqueteos/webbrowser v1.2.0 github.com/twmb/murmur3 v1.1.8 github.com/urfave/negroni v1.0.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 - go.opentelemetry.io/otel v1.28.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 - go.opentelemetry.io/otel/sdk v1.28.0 - go.opentelemetry.io/otel/trace v1.28.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 + go.opentelemetry.io/otel v1.32.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 + go.opentelemetry.io/otel/sdk v1.32.0 + go.opentelemetry.io/otel/trace v1.32.0 go.uber.org/automaxprocs v1.5.3 - golang.org/x/crypto v0.25.0 + golang.org/x/crypto v0.28.0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 - golang.org/x/oauth2 v0.21.0 - golang.org/x/sync v0.7.0 + golang.org/x/oauth2 v0.23.0 + golang.org/x/sync v0.9.0 golang.org/x/tools v0.23.0 ) -require github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - require ( code.dny.dev/ssrf v0.2.0 // indirect dario.cat/mergo v1.0.0 // indirect @@ -91,13 +96,13 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/cockroachdb/cockroach-go/v2 v2.3.5 // indirect github.com/containerd/continuity v0.4.3 // indirect github.com/cristalhq/jwt/v4 v4.0.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/dgraph-io/ristretto v1.0.0 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/cli v26.1.4+incompatible // indirect @@ -138,7 +143,6 @@ require ( github.com/goccy/go-yaml v1.11.3 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/glog v1.2.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/pprof v0.0.0-20230808223545-4887780b67fb // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect @@ -146,7 +150,8 @@ require ( github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/huandu/xstrings v1.4.0 // indirect github.com/imdario/mergo v0.3.16 // indirect @@ -192,7 +197,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/runc v1.1.14 // indirect - github.com/openzipkin/zipkin-go v0.4.2 // indirect + github.com/openzipkin/zipkin-go v0.4.3 // indirect github.com/ory/dockertest/v3 v3.10.1-0.20240704115616-d229e74b748d // indirect github.com/ory/go-convenience v0.1.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect @@ -202,7 +207,7 @@ require ( github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 // indirect @@ -225,25 +230,25 @@ require ( github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect github.com/yuin/gopher-lua v1.1.1 // indirect go.mongodb.org/mongo-driver v1.14.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 // indirect - go.opentelemetry.io/contrib/propagators/b3 v1.21.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.21.1 // indirect - go.opentelemetry.io/contrib/samplers/jaegerremote v0.15.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.57.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.32.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.32.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.26.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/zipkin v1.21.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/zipkin v1.32.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.19.0 // indirect - golang.org/x/net v0.27.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect - google.golang.org/grpc v1.64.1 // indirect - google.golang.org/protobuf v1.34.2 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.35.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 53e07d5b53d..0e873c66ffe 100644 --- a/go.sum +++ b/go.sum @@ -51,9 +51,8 @@ github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTx github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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= @@ -76,10 +75,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= -github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84= +github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -92,7 +91,6 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elliotchance/orderedmap v1.6.0 h1:xjn+kbbKXeDq6v9RVE+WYwRbYfAZKvlWfcJNxM8pvEw= @@ -167,8 +165,6 @@ github.com/gobuffalo/github_flavored_markdown v1.1.4 h1:WacrEGPXUDX+BpU1GM/Y0ADg github.com/gobuffalo/github_flavored_markdown v1.1.4/go.mod h1:Vl9686qrVVQou4GrHRK/KOG3jCZOKLUqV8MMOAYtlso= github.com/gobuffalo/helpers v0.6.7 h1:C9CedoRSfgWg2ZoIkVXgjI5kgmSpL34Z3qdnzpfNVd8= github.com/gobuffalo/helpers v0.6.7/go.mod h1:j0u1iC1VqlCaJEEVkZN8Ia3TEzfj/zoXANqyJExTMTA= -github.com/gobuffalo/here v0.6.7 h1:hpfhh+kt2y9JLDfhYUxxCRxQol540jsVfKUZzjlbp8o= -github.com/gobuffalo/here v0.6.7/go.mod h1:vuCfanjqckTuRlqAitJz6QC4ABNnS27wLb816UhsPcc= github.com/gobuffalo/httptest v1.5.2 h1:GpGy520SfY1QEmyPvaqmznTpG4gEQqQ82HtHqyNEreM= github.com/gobuffalo/httptest v1.5.2/go.mod h1:FA23yjsWLGj92mVV74Qtc8eqluc11VqcWr8/C1vxt4g= github.com/gobuffalo/nulls v0.4.2 h1:GAqBR29R3oPY+WCC7JL9KKk9erchaNuV6unsOSZGQkw= @@ -195,9 +191,6 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= -github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= 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.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -229,8 +222,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= @@ -331,8 +324,6 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCno= -github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -381,14 +372,14 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w= github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= -github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA= -github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY= +github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= +github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= github.com/ory/analytics-go/v5 v5.0.1 h1:LX8T5B9FN8KZXOtxgN+R3I4THRRVB6+28IKgKBpXmAM= github.com/ory/analytics-go/v5 v5.0.1/go.mod h1:lWCiCjAaJkKfgR/BN5DCLMol8BjKS1x+4jxBxff/FF0= github.com/ory/dockertest/v3 v3.10.1-0.20240704115616-d229e74b748d h1:By96ZSVuH5LyjXLVVMfvJoLVGHaT96LdOnwgFSLVf0E= github.com/ory/dockertest/v3 v3.10.1-0.20240704115616-d229e74b748d/go.mod h1:F2FIjwwAk6CsNAs//B8+aPFQF0t84pbM8oliyNXwQrk= -github.com/ory/fosite v0.47.0 h1:Iqu5uhx54JqZQPn2hRhqjESrmRRyQb00uJjfEi1a1QI= -github.com/ory/fosite v0.47.0/go.mod h1:5U6c9nOLxyTdD/qrFv7N88TSxkdk5Wq8NzvB7UViDP0= +github.com/ory/fosite v0.47.1-0.20241101073333-eab241e153a4 h1:1pEVHGC+Dx2xMPMgpRgG3lyejyK8iU9KKfSnLowLYd8= +github.com/ory/fosite v0.47.1-0.20241101073333-eab241e153a4/go.mod h1:AZyn1jrABUaGN12RHcWorRLbqLn52gTdHaIYY81m5J0= github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe h1:rvu4obdvqR0fkSIJ8IfgzKOWwZ5kOT2UNfLq81Qk7rc= github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe/go.mod h1:z4n3u6as84LbV4YmgjHhnwtccQqzf4cZlSk9f1FhygI= github.com/ory/go-convenience v0.1.0 h1:zouLKfF2GoSGnJwGq+PE/nJAE6dj2Zj5QlTgmMTsTS8= @@ -401,10 +392,10 @@ github.com/ory/jsonschema/v3 v3.0.8 h1:Ssdb3eJ4lDZ/+XnGkvQS/te0p+EkolqwTsDOCxr/F github.com/ory/jsonschema/v3 v3.0.8/go.mod h1:ZPzqjDkwd3QTnb2Z6PAS+OTvBE2x5i6m25wCGx54W/0= github.com/ory/kratos-client-go v1.2.1 h1:Q3T/adfAfAkHFcV1LGLnwz4QkY6ghBdX9zde5T8uO/4= github.com/ory/kratos-client-go v1.2.1/go.mod h1:WiQYlrqW4Atj6Js7oDN5ArbZxo0nTO2u/e1XaDv2yMI= -github.com/ory/pop/v6 v6.2.0 h1:hRFOGAOEHw91kUHQ32k5NHqCkcHrRou/romvrJP1w0E= -github.com/ory/pop/v6 v6.2.0/go.mod h1:okVAYKGtgunD/wbW3NGhZTndJCS+6FqO+cA89rQ4doc= -github.com/ory/x v0.0.647 h1:DVKgA3ykMB9qXuMdSl5C8SFWr3yw7Xe8jpSm0+iqGeU= -github.com/ory/x v0.0.647/go.mod h1:M+0EAXo7DT7Z2/Yrzvh4mgxOoV1fGI1jOKyAJ72d4Qs= +github.com/ory/pop/v6 v6.2.1-0.20241121111754-e5dfc0f3344b h1:BIzoOe2/wynZBQak1po0tzgvARseIKsR2bF6b+SZoKE= +github.com/ory/pop/v6 v6.2.1-0.20241121111754-e5dfc0f3344b/go.mod h1:okVAYKGtgunD/wbW3NGhZTndJCS+6FqO+cA89rQ4doc= +github.com/ory/x v0.0.675 h1:K6GpVo99BXBFv2UiwMjySNNNqCFKGswynrt7vWQJFU8= +github.com/ory/x v0.0.675/go.mod h1:zJmnDtKje2FCP4EeFvRsKk94XXiqKCSGJMZcirAfhUs= github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= @@ -435,8 +426,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -537,32 +528,32 @@ github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/contrib/propagators/b3 v1.21.0 h1:uGdgDPNzwQWRwCXJgw/7h29JaRqcq9B87Iv4hJDKAZw= -go.opentelemetry.io/contrib/propagators/b3 v1.21.0/go.mod h1:D9GQXvVGT2pzyTfp1QBOnD1rzKEWzKjjwu5q2mslCUI= -go.opentelemetry.io/contrib/propagators/jaeger v1.21.1 h1:f4beMGDKiVzg9IcX7/VuWVy+oGdjx3dNJ72YehmtY5k= -go.opentelemetry.io/contrib/propagators/jaeger v1.21.1/go.mod h1:U9jhkEl8d1LL+QXY7q3kneJWJugiN3kZJV2OWz3hkBY= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.15.1 h1:Qb+5A+JbIjXwO7l4HkRUhgIn4Bzz0GNS2q+qdmSx+0c= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.15.1/go.mod h1:G4vNCm7fRk0kjZ6pGNLo5SpLxAUvOfSrcaegnT8TPck= -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/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.57.0 h1:7F3XCD6WYzDkwbi8I8N+oYJWquPVScnRosKGgqjsR8c= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.57.0/go.mod h1:Dk3C0BfIlZDZ5c6eVS7TYiH2vssuyUU3vUsgbrR+5V4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= +go.opentelemetry.io/contrib/propagators/b3 v1.32.0 h1:MazJBz2Zf6HTN/nK/s3Ru1qme+VhWU5hm83QxEP+dvw= +go.opentelemetry.io/contrib/propagators/b3 v1.32.0/go.mod h1:B0s70QHYPrJwPOwD1o3V/R8vETNOG9N3qZf4LDYvA30= +go.opentelemetry.io/contrib/propagators/jaeger v1.32.0 h1:K/fOyTMD6GELKTIJBaJ9k3ppF2Njt8MeUGBOwfaWXXA= +go.opentelemetry.io/contrib/propagators/jaeger v1.32.0/go.mod h1:ISE6hda//MTWvtngG7p4et3OCngsrTVfl7c6DjN17f8= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.26.0 h1:/SKXyZLAnuj981HVc8G5ZylYK3qD2W6AYR6cJx5kIHw= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.26.0/go.mod h1:cOEzME0M2OKeHB45lJiOKfvUCdg/r75mf7YS5w0tbmE= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk= -go.opentelemetry.io/otel/exporters/zipkin v1.21.0 h1:D+Gv6lSfrFBWmQYyxKjDd0Zuld9SRXpIrEsKZvE4DO4= -go.opentelemetry.io/otel/exporters/zipkin v1.21.0/go.mod h1:83oMKR6DzmHisFOW3I+yIMGZUTjxiWaiBI8M8+TU5zE= -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/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= -go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= -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/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= +go.opentelemetry.io/otel/exporters/zipkin v1.32.0 h1:6O8HgLHPXtXE9QEKEWkBImL9mEKCGEl+m+OncVO53go= +go.opentelemetry.io/otel/exporters/zipkin v1.32.0/go.mod h1:+MFvorlowjy0iWnsKaNxC1kzczSxe71mw85h4p8yEvg= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= @@ -579,8 +570,8 @@ golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4 golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -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.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -606,10 +597,10 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= -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/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -618,8 +609,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -635,7 +626,6 @@ 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.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -643,8 +633,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.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.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -661,8 +651,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.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/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/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -681,16 +671,16 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= -google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= -google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= -google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc/examples v0.0.0-20210304020650-930c79186c99 h1:qA8rMbz1wQ4DOFfM2ouD29DG9aHWBm6ZOy9BGxiUMmY= google.golang.org/grpc/examples v0.0.0-20210304020650-930c79186c99/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/config/config.yaml b/internal/config/config.yaml index f3e8bff399c..49615d95966 100644 --- a/internal/config/config.yaml +++ b/internal/config/config.yaml @@ -402,6 +402,18 @@ oauth2: session: # store encrypted data in database, default true encrypt_at_rest: true + ## refresh_token_rotation + # By default Refresh Tokens are rotated and invalidated with each use. See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.13.2 for more details + refresh_token_rotation: + # + ## grace_period + # + # Set the grace period for refresh tokens to be reused. Such reused tokens will result in multiple refresh tokens being issued. + # + # Examples: + # - 5s + # - 1m + grace_period: 0s # The secrets section configures secrets used for encryption and signing of several systems. All secrets can be rotated, # for more information on this topic navigate to: diff --git a/internal/kratos/fake_kratos.go b/internal/kratos/fake_kratos.go index bffad0696f0..2303320c305 100644 --- a/internal/kratos/fake_kratos.go +++ b/internal/kratos/fake_kratos.go @@ -7,6 +7,7 @@ import ( "context" "github.com/ory/fosite" + client "github.com/ory/kratos-client-go" ) type ( @@ -17,9 +18,10 @@ type ( ) const ( - FakeSessionID = "fake-kratos-session-id" - FakeUsername = "fake-kratos-username" - FakePassword = "fake-kratos-password" // nolint: gosec + FakeSessionID = "fake-kratos-session-id" + FakeUsername = "fake-kratos-username" + FakePassword = "fake-kratos-password" // nolint: gosec + FakeIdentityID = "fake-kratos-identity-id" ) var _ Client = new(FakeKratos) @@ -35,11 +37,11 @@ func (f *FakeKratos) DisableSession(_ context.Context, identityProviderSessionID return nil } -func (f *FakeKratos) Authenticate(_ context.Context, username, password string) error { +func (f *FakeKratos) Authenticate(_ context.Context, username, password string) (*client.Session, error) { if username == FakeUsername && password == FakePassword { - return nil + return &client.Session{Identity: &client.Identity{Id: FakeIdentityID}}, nil } - return fosite.ErrNotFound + return nil, fosite.ErrNotFound } func (f *FakeKratos) Reset() { diff --git a/internal/kratos/kratos.go b/internal/kratos/kratos.go index c6d7be034ce..04e8fbfcdfb 100644 --- a/internal/kratos/kratos.go +++ b/internal/kratos/kratos.go @@ -31,7 +31,7 @@ type ( } Client interface { DisableSession(ctx context.Context, identityProviderSessionID string) error - Authenticate(ctx context.Context, name, secret string) error + Authenticate(ctx context.Context, name, secret string) (*client.Session, error) } Default struct { dependencies @@ -42,7 +42,7 @@ func New(d dependencies) Client { return &Default{dependencies: d} } -func (k *Default) Authenticate(ctx context.Context, name, secret string) (err error) { +func (k *Default) Authenticate(ctx context.Context, name, secret string) (session *client.Session, err error) { ctx, span := k.Tracer(ctx).Tracer().Start(ctx, "kratos.Authenticate") otelx.End(span, &err) @@ -52,17 +52,17 @@ func (k *Default) Authenticate(ctx context.Context, name, secret string) (err er span.SetAttributes(attribute.Bool("skipped", true)) span.SetAttributes(attribute.String("reason", "kratos public url not set")) - return errors.New("kratos public url not set") + return nil, errors.New("kratos public url not set") } kratos := k.newKratosClient(ctx, publicURL) flow, _, err := kratos.FrontendAPI.CreateNativeLoginFlow(ctx).Execute() if err != nil { - return err + return nil, err } - _, _, err = kratos.FrontendAPI.UpdateLoginFlow(ctx).Flow(flow.Id).UpdateLoginFlowBody(client.UpdateLoginFlowBody{ + res, _, err := kratos.FrontendAPI.UpdateLoginFlow(ctx).Flow(flow.Id).UpdateLoginFlowBody(client.UpdateLoginFlowBody{ UpdateLoginFlowWithPasswordMethod: &client.UpdateLoginFlowWithPasswordMethod{ Method: "password", Identifier: name, @@ -70,10 +70,10 @@ func (k *Default) Authenticate(ctx context.Context, name, secret string) (err er }, }).Execute() if err != nil { - return fosite.ErrNotFound.WithWrap(err) + return nil, fosite.ErrNotFound.WithWrap(err) } - return nil + return &res.Session, nil } func (k *Default) DisableSession(ctx context.Context, identityProviderSessionID string) (err error) { diff --git a/jwk/jwt_strategy.go b/jwk/jwt_strategy.go index 6154066459b..f8ca396a1df 100644 --- a/jwk/jwt_strategy.go +++ b/jwk/jwt_strategy.go @@ -53,7 +53,7 @@ func (j *DefaultJWTSigner) getKeys(ctx context.Context) (private *jose.JSONWebKe return nil, errors.WithStack(fosite.ErrServerError. WithWrap(err). - WithHintf(`Could not ensure that signing keys for "%s" exists. If you are running against a persistent SQL database this is most likely because your "secrets.system" ("SECRETS_SYSTEM" environment variable) is not set or changed. When running with an SQL database backend you need to make sure that the secret is set and stays the same, unless when doing key rotation. This may also happen when you forget to run "hydra migrate sql..`, j.setID)) + WithHintf(`Could not ensure that signing keys for "%s" exists. If you are running against a persistent SQL database this is most likely because your "secrets.system" ("SECRETS_SYSTEM" environment variable) is not set or changed. When running with an SQL database backend you need to make sure that the secret is set and stays the same, unless when doing key rotation. This may also happen when you forget to run "hydra migrate sql up -e".`, j.setID)) } func (j *DefaultJWTSigner) GetPublicKeyID(ctx context.Context) (string, error) { diff --git a/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=false.json b/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=false.json index 215fa018214..5bc92ec79a5 100644 --- a/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=false.json +++ b/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=false.json @@ -63,7 +63,8 @@ "require_request_uri_registration": true, "response_modes_supported": [ "query", - "fragment" + "fragment", + "form_post" ], "response_types_supported": [ "code", diff --git a/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=true.json b/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=true.json index 215fa018214..5bc92ec79a5 100644 --- a/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=true.json +++ b/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=true.json @@ -63,7 +63,8 @@ "require_request_uri_registration": true, "response_modes_supported": [ "query", - "fragment" + "fragment", + "form_post" ], "response_types_supported": [ "code", diff --git a/oauth2/flowctx/cookies.go b/oauth2/flowctx/cookies.go deleted file mode 100644 index 42609d50019..00000000000 --- a/oauth2/flowctx/cookies.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -package flowctx - -import "github.com/ory/hydra/v2/client" - -type ( - CookieSuffixer interface { - CookieSuffix() string - } - - StaticSuffix string - clientID string -) - -func (s StaticSuffix) CookieSuffix() string { return string(s) } -func (s clientID) GetID() string { return string(s) } - -const ( - loginSessionCookie = "ory_hydra_loginsession" -) - -func LoginSessionCookie(suffix CookieSuffixer) string { - return loginSessionCookie + "_" + suffix.CookieSuffix() -} - -func SuffixForClient(c client.IDer) StaticSuffix { - return StaticSuffix(client.CookieSuffix(c)) -} - -func SuffixFromStatic(id string) StaticSuffix { - return SuffixForClient(clientID(id)) -} diff --git a/oauth2/flowctx/encoding.go b/oauth2/flowctx/encoding.go index 67d9d51d2a6..8a1f8cbf270 100644 --- a/oauth2/flowctx/encoding.go +++ b/oauth2/flowctx/encoding.go @@ -8,13 +8,10 @@ import ( "compress/gzip" "context" "encoding/json" - "net/http" "github.com/pkg/errors" - "github.com/ory/fosite" "github.com/ory/hydra/v2/aead" - "github.com/ory/hydra/v2/driver/config" ) type ( @@ -84,50 +81,21 @@ func Encode(ctx context.Context, cipher aead.Cipher, val any, opts ...CodecOptio // Steps: // 1. Encode to JSON // 2. GZIP - // 3. Encrypt with AEAD (AES-GCM) + Base64 URL-encode + // 3. Encrypt with AEAD (XChaCha20-Poly1305) + Base64 URL-encode var b bytes.Buffer - gz := gzip.NewWriter(&b) + gz, err := gzip.NewWriterLevel(&b, gzip.BestCompression) + if err != nil { + return "", err + } if err = json.NewEncoder(gz).Encode(val); err != nil { return "", err } + if err = gz.Close(); err != nil { return "", err } return cipher.Encrypt(ctx, b.Bytes(), additionalDataFromOpts(opts...)) } - -// SetCookie encrypts the given value and sets it in a cookie. -func SetCookie(ctx context.Context, w http.ResponseWriter, reg interface { - FlowCipher() *aead.XChaCha20Poly1305 - config.Provider -}, cookieName string, value any, opts ...CodecOption) error { - cipher := reg.FlowCipher() - cookie, err := Encode(ctx, cipher, value, opts...) - if err != nil { - return err - } - - http.SetCookie(w, &http.Cookie{ - Name: cookieName, - Value: cookie, - HttpOnly: true, - Domain: reg.Config().CookieDomain(ctx), - Secure: reg.Config().CookieSecure(ctx), - SameSite: reg.Config().CookieSameSiteMode(ctx), - }) - - return nil -} - -// FromCookie looks up the value stored in the cookie and decodes it. -func FromCookie[T any](ctx context.Context, r *http.Request, cipher aead.Cipher, cookieName string, opts ...CodecOption) (*T, error) { - cookie, err := r.Cookie(cookieName) - if err != nil { - return nil, errors.WithStack(fosite.ErrInvalidClient.WithHint("No cookie found for this request. Please initiate a new flow and retry.")) - } - - return Decode[T](ctx, cipher, cookie.Value, opts...) -} diff --git a/oauth2/flowctx/encoding_test.go b/oauth2/flowctx/encoding_test.go new file mode 100644 index 00000000000..4d2ee89e62a --- /dev/null +++ b/oauth2/flowctx/encoding_test.go @@ -0,0 +1,134 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flowctx_test + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/require" + + "github.com/ory/hydra/v2/aead" + "github.com/ory/hydra/v2/client" + "github.com/ory/hydra/v2/flow" + "github.com/ory/hydra/v2/oauth2/flowctx" + "github.com/ory/x/pointerx" + "github.com/ory/x/sqlxx" +) + +func TestEncoding(t *testing.T) { + f := flow.Flow{ + ID: uuid.Must(uuid.NewV4()).String(), + NID: uuid.Must(uuid.NewV4()), + RequestedScope: []string{"scope1", "scope2"}, + RequestedAudience: []string{"https://api.example.org/v1", "https://api.example.org/v2"}, + LoginSkip: false, + Subject: "some-subject@some-idp-somewhere.com", + OpenIDConnectContext: &flow.OAuth2ConsentRequestOpenIDConnectContext{ + ACRValues: []string{"acr1", "acr2"}, + UILocales: []string{"en-US", "en-GB"}, + Display: "page", + IDTokenHintClaims: map[string]interface{}{"claim1": "value1", "claim2": "value2"}, + LoginHint: "some-login-hint", + }, + Client: &client.Client{ + ID: uuid.Must(uuid.NewV4()).String(), + NID: uuid.Must(uuid.NewV4()), + Name: "some-client-name", + Secret: "some-supersafe-secret", + RedirectURIs: []string{ + "https://redirect1.example.org/callback", + "https://redirect2.example.org/callback", + }, + GrantTypes: []string{"authorization_code", "refresh_token"}, + ResponseTypes: []string{"code"}, + Scope: "scope1 scope2", + Audience: sqlxx.StringSliceJSONFormat{"https://api.example.org/v1 https://api.example.org/v2"}, + Owner: "some-owner", + TermsOfServiceURI: "https://tos.example.org", + PolicyURI: "https://policy.example.org", + ClientURI: "https://client.example.org", + LogoURI: "https://logo.example.org", + Contacts: []string{"contact1", "contact2"}, + SubjectType: "public", + JSONWebKeysURI: "https://jwks.example.org", + JSONWebKeys: nil, // TODO? + TokenEndpointAuthMethod: "client_secret_basic", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + AllowedCORSOrigins: []string{"https://cors1.example.org", "https://cors2.example.org"}, + Metadata: sqlxx.JSONRawMessage(`{"client-metadata-key1": "val1"}`), + AccessTokenStrategy: "jwt", + SkipConsent: true, + }, + RequestURL: "https://auth.hydra.local/oauth2/auth?client_id=some-client-id&response_type=code&scope=scope1+scope2&redirect_uri=https%3A%2F%2Fredirect1.example.org%2Fcallback&state=some-state&nonce=some-nonce", + SessionID: sqlxx.NullString("some-session-id"), + LoginCSRF: uuid.Must(uuid.NewV4()).String(), + LoginInitializedAt: sqlxx.NullTime(time.Now()), + RequestedAt: time.Now(), + State: 1, + LoginRemember: true, + LoginRememberFor: 3600, + Context: sqlxx.JSONRawMessage(`{"context-key1": "val1"}`), + GrantedScope: []string{"scope1", "scope2"}, + GrantedAudience: []string{"https://api.example.org/v1", "https://api.example.org/v2"}, + ConsentRemember: true, + ConsentRememberFor: pointerx.Int(3600), + ConsentHandledAt: sqlxx.NullTime(time.Now()), + SessionIDToken: sqlxx.MapStringInterface{ + "session-id-token-key1": "val1", + "session-id-token-key2": "val2", + uuid.Must(uuid.NewV4()).String(): "val3", + uuid.Must(uuid.NewV4()).String(): "val4", + uuid.Must(uuid.NewV4()).String(): "val5", + }, + SessionAccessToken: sqlxx.MapStringInterface{ + "session-access-token-key1": "val1", + "session-access-token-key2": "val2", + uuid.Must(uuid.NewV4()).String(): "val3", + uuid.Must(uuid.NewV4()).String(): "val4", + uuid.Must(uuid.NewV4()).String(): "val5", + }, + } + + ctx := context.Background() + + t.Run("with client", func(t *testing.T) { + j, err := json.Marshal(f) + require.NoError(t, err) + t.Logf("Length (JSON): %d", len(j)) + cp := new(cipherProvider) + consentVerifier, err := flowctx.Encode(ctx, cp.FlowCipher(), f, flowctx.AsConsentVerifier) + require.NoError(t, err) + t.Logf("Length (JSON+GZIP+AEAD): %d", len(consentVerifier)) + }) + t.Run("without client", func(t *testing.T) { + f := f + f.Client = nil + j, err := json.Marshal(f) + require.NoError(t, err) + t.Logf("Length (JSON): %d", len(j)) + cp := new(cipherProvider) + consentVerifier, err := f.ToConsentVerifier(ctx, cp) + require.NoError(t, err) + t.Logf("Length (JSON+GZIP+AEAD): %d", len(consentVerifier)) + }) +} + +type cipherProvider struct{} + +func (c *cipherProvider) FlowCipher() *aead.XChaCha20Poly1305 { + return aead.NewXChaCha20Poly1305(c) +} + +func (c *cipherProvider) GetGlobalSecret(context.Context) ([]byte, error) { + return []byte("supersecret123456789123456789012"), nil +} + +func (c *cipherProvider) GetRotatedGlobalSecrets(ctx context.Context) ([][]byte, error) { + return nil, nil +} diff --git a/oauth2/fosite_store_helpers.go b/oauth2/fosite_store_helpers.go index 6b64d61991e..553a6bae62b 100644 --- a/oauth2/fosite_store_helpers.go +++ b/oauth2/fosite_store_helpers.go @@ -25,6 +25,7 @@ import ( "github.com/ory/hydra/v2/oauth2/trust" + "github.com/ory/hydra/v2/driver/config" "github.com/ory/hydra/v2/x" "github.com/ory/fosite/storage" @@ -225,16 +226,18 @@ func TestHelperRunner(t *testing.T, store InternalRegistry, k string) { t.Run(fmt.Sprintf("case=testHelperDeleteAccessTokens/db=%s", k), testHelperDeleteAccessTokens(store)) t.Run(fmt.Sprintf("case=testHelperRevokeAccessToken/db=%s", k), testHelperRevokeAccessToken(store)) t.Run(fmt.Sprintf("case=testFositeJWTBearerGrantStorage/db=%s", k), testFositeJWTBearerGrantStorage(store)) + t.Run(fmt.Sprintf("case=testHelperRevokeRefreshTokenMaybeGracePeriod/db=%s", k), testHelperRevokeRefreshTokenMaybeGracePeriod(store)) } func testHelperRequestIDMultiples(m InternalRegistry, _ string) func(t *testing.T) { return func(t *testing.T) { - requestId := uuid.New() - mockRequestForeignKey(t, requestId, m) + ctx := context.Background() + requestID := uuid.New() + mockRequestForeignKey(t, requestID, m) cl := &client.Client{ID: "foobar"} fositeRequest := &fosite.Request{ - ID: requestId, + ID: requestID, Client: cl, RequestedAt: time.Now().UTC().Round(time.Second), Session: NewSession("bar"), @@ -242,15 +245,15 @@ func testHelperRequestIDMultiples(m InternalRegistry, _ string) func(t *testing. for i := 0; i < 4; i++ { signature := uuid.New() - err := m.OAuth2Storage().CreateRefreshTokenSession(context.TODO(), signature, fositeRequest) + err := m.OAuth2Storage().CreateRefreshTokenSession(ctx, signature, fositeRequest) assert.NoError(t, err) - err = m.OAuth2Storage().CreateAccessTokenSession(context.TODO(), signature, fositeRequest) + err = m.OAuth2Storage().CreateAccessTokenSession(ctx, signature, fositeRequest) assert.NoError(t, err) - err = m.OAuth2Storage().CreateOpenIDConnectSession(context.TODO(), signature, fositeRequest) + err = m.OAuth2Storage().CreateOpenIDConnectSession(ctx, signature, fositeRequest) assert.NoError(t, err) - err = m.OAuth2Storage().CreatePKCERequestSession(context.TODO(), signature, fositeRequest) + err = m.OAuth2Storage().CreatePKCERequestSession(ctx, signature, fositeRequest) assert.NoError(t, err) - err = m.OAuth2Storage().CreateAuthorizeCodeSession(context.TODO(), signature, fositeRequest) + err = m.OAuth2Storage().CreateAuthorizeCodeSession(ctx, signature, fositeRequest) assert.NoError(t, err) } } @@ -475,7 +478,7 @@ func testHelperNilAccessToken(x InternalRegistry) func(t *testing.T) { m := x.OAuth2Storage() c := &client.Client{ID: "nil-request-client-id-123"} require.NoError(t, x.ClientManager().CreateClient(context.Background(), c)) - err := m.CreateAccessTokenSession(context.TODO(), "nil-request-id", &fosite.Request{ + err := m.CreateAccessTokenSession(context.Background(), "nil-request-id", &fosite.Request{ ID: "", RequestedAt: time.Now().UTC().Round(time.Second), Client: c, @@ -553,6 +556,63 @@ func testHelperRevokeAccessToken(x InternalRegistry) func(t *testing.T) { } } +func testHelperRevokeRefreshTokenMaybeGracePeriod(x InternalRegistry) func(t *testing.T) { + return func(t *testing.T) { + ctx := context.Background() + + t.Run("Revokes refresh token when grace period not configured", func(t *testing.T) { + // SETUP + m := x.OAuth2Storage() + + refreshTokenSession := fmt.Sprintf("refresh_token_%d", time.Now().Unix()) + err := m.CreateRefreshTokenSession(ctx, refreshTokenSession, &defaultRequest) + require.NoError(t, err, "precondition failed: could not create refresh token session") + + // ACT + err = m.RevokeRefreshTokenMaybeGracePeriod(ctx, defaultRequest.GetID(), refreshTokenSession) + require.NoError(t, err) + + tmpSession := new(fosite.Session) + _, err = m.GetRefreshTokenSession(ctx, refreshTokenSession, *tmpSession) + + // ASSERT + // a revoked refresh token returns an error when getting the token again + assert.ErrorIs(t, err, fosite.ErrInactiveToken) + }) + + t.Run("refresh token enters grace period when configured,", func(t *testing.T) { + // SETUP + x.Config().MustSet(ctx, config.KeyRefreshTokenRotationGracePeriod, "1m") + + // always reset back to the default + t.Cleanup(func() { + x.Config().MustSet(ctx, config.KeyRefreshTokenRotationGracePeriod, "0m") + }) + + m := x.OAuth2Storage() + + refreshTokenSession := fmt.Sprintf("refresh_token_%d_with_grace_period", time.Now().Unix()) + err := m.CreateRefreshTokenSession(ctx, refreshTokenSession, &defaultRequest) + require.NoError(t, err, "precondition failed: could not create refresh token session") + + // ACT + require.NoError(t, m.RevokeRefreshTokenMaybeGracePeriod(ctx, defaultRequest.GetID(), refreshTokenSession)) + require.NoError(t, m.RevokeRefreshTokenMaybeGracePeriod(ctx, defaultRequest.GetID(), refreshTokenSession)) + require.NoError(t, m.RevokeRefreshTokenMaybeGracePeriod(ctx, defaultRequest.GetID(), refreshTokenSession)) + + req, err := m.GetRefreshTokenSession(ctx, refreshTokenSession, nil) + + // ASSERT + // when grace period is configured the refresh token can be obtained within + // the grace period without error + assert.NoError(t, err) + + assert.Equal(t, defaultRequest.GetID(), req.GetID()) + }) + } + +} + func testHelperCreateGetDeletePKCERequestSession(x InternalRegistry) func(t *testing.T) { return func(t *testing.T) { m := x.OAuth2Storage() @@ -880,6 +940,7 @@ func testFositeStoreClientAssertionJWTValid(m InternalRegistry) func(*testing.T) func testFositeJWTBearerGrantStorage(x InternalRegistry) func(t *testing.T) { return func(t *testing.T) { + ctx := context.Background() grantManager := x.GrantManager() keyManager := x.KeyManager() grantStorage := x.OAuth2Storage().(rfc7523.RFC7523KeyStorage) @@ -902,28 +963,28 @@ func testFositeJWTBearerGrantStorage(x InternalRegistry) func(t *testing.T) { ExpiresAt: time.Now().UTC().Round(time.Second).AddDate(1, 0, 0), } - storedKeySet, err := grantStorage.GetPublicKeys(context.TODO(), issuer, subject) + storedKeySet, err := grantStorage.GetPublicKeys(ctx, issuer, subject) require.NoError(t, err) require.Len(t, storedKeySet.Keys, 0) - err = grantManager.CreateGrant(context.TODO(), grant, publicKey) + err = grantManager.CreateGrant(ctx, grant, publicKey) require.NoError(t, err) - storedKeySet, err = grantStorage.GetPublicKeys(context.TODO(), issuer, subject) + storedKeySet, err = grantStorage.GetPublicKeys(ctx, issuer, subject) require.NoError(t, err) assert.Len(t, storedKeySet.Keys, 1) - storedKey, err := grantStorage.GetPublicKey(context.TODO(), issuer, subject, publicKey.KeyID) + storedKey, err := grantStorage.GetPublicKey(ctx, issuer, subject, publicKey.KeyID) require.NoError(t, err) assert.Equal(t, publicKey.KeyID, storedKey.KeyID) assert.Equal(t, publicKey.Use, storedKey.Use) assert.Equal(t, publicKey.Key, storedKey.Key) - storedScopes, err := grantStorage.GetPublicKeyScopes(context.TODO(), issuer, subject, publicKey.KeyID) + storedScopes, err := grantStorage.GetPublicKeyScopes(ctx, issuer, subject, publicKey.KeyID) require.NoError(t, err) assert.Equal(t, grant.Scope, storedScopes) - storedKeySet, err = keyManager.GetKey(context.TODO(), issuer, publicKey.KeyID) + storedKeySet, err = keyManager.GetKey(ctx, issuer, publicKey.KeyID) require.NoError(t, err) assert.Equal(t, publicKey.KeyID, storedKeySet.Keys[0].KeyID) assert.Equal(t, publicKey.Use, storedKeySet.Keys[0].Use) @@ -953,7 +1014,7 @@ func testFositeJWTBearerGrantStorage(x InternalRegistry) func(t *testing.T) { keySet2ToReturn, err := jwk.GenerateJWK(context.Background(), jose.ES256, "maria-key-2", "sig") require.NoError(t, err) - require.NoError(t, grantManager.CreateGrant(context.TODO(), trust.Grant{ + require.NoError(t, grantManager.CreateGrant(ctx, trust.Grant{ ID: uuid.New(), Issuer: issuer, Subject: subject, @@ -1011,22 +1072,22 @@ func testFositeJWTBearerGrantStorage(x InternalRegistry) func(t *testing.T) { ExpiresAt: time.Now().UTC().Round(time.Second).AddDate(1, 0, 0), } - err = grantManager.CreateGrant(context.TODO(), grant, publicKey) + err = grantManager.CreateGrant(ctx, grant, publicKey) require.NoError(t, err) - _, err = grantStorage.GetPublicKey(context.TODO(), issuer, subject, grant.PublicKey.KeyID) + _, err = grantStorage.GetPublicKey(ctx, issuer, subject, grant.PublicKey.KeyID) require.NoError(t, err) - _, err = keyManager.GetKey(context.TODO(), issuer, publicKey.KeyID) + _, err = keyManager.GetKey(ctx, issuer, publicKey.KeyID) require.NoError(t, err) - err = grantManager.DeleteGrant(context.TODO(), grant.ID) + err = grantManager.DeleteGrant(ctx, grant.ID) require.NoError(t, err) - _, err = grantStorage.GetPublicKey(context.TODO(), issuer, subject, publicKey.KeyID) + _, err = grantStorage.GetPublicKey(ctx, issuer, subject, publicKey.KeyID) assert.Error(t, err) - _, err = keyManager.GetKey(context.TODO(), issuer, publicKey.KeyID) + _, err = keyManager.GetKey(ctx, issuer, publicKey.KeyID) assert.Error(t, err) }) @@ -1048,22 +1109,22 @@ func testFositeJWTBearerGrantStorage(x InternalRegistry) func(t *testing.T) { ExpiresAt: time.Now().UTC().Round(time.Second).AddDate(1, 0, 0), } - err = grantManager.CreateGrant(context.TODO(), grant, publicKey) + err = grantManager.CreateGrant(ctx, grant, publicKey) require.NoError(t, err) - _, err = grantStorage.GetPublicKey(context.TODO(), issuer, subject, publicKey.KeyID) + _, err = grantStorage.GetPublicKey(ctx, issuer, subject, publicKey.KeyID) require.NoError(t, err) - _, err = keyManager.GetKey(context.TODO(), issuer, publicKey.KeyID) + _, err = keyManager.GetKey(ctx, issuer, publicKey.KeyID) require.NoError(t, err) - err = keyManager.DeleteKey(context.TODO(), issuer, publicKey.KeyID) + err = keyManager.DeleteKey(ctx, issuer, publicKey.KeyID) require.NoError(t, err) - _, err = keyManager.GetKey(context.TODO(), issuer, publicKey.KeyID) + _, err = keyManager.GetKey(ctx, issuer, publicKey.KeyID) assert.Error(t, err) - _, err = grantManager.GetConcreteGrant(context.TODO(), grant.ID) + _, err = grantManager.GetConcreteGrant(ctx, grant.ID) assert.Error(t, err) }) @@ -1085,25 +1146,25 @@ func testFositeJWTBearerGrantStorage(x InternalRegistry) func(t *testing.T) { ExpiresAt: time.Now().UTC().Round(time.Second).AddDate(1, 0, 0), } - err = grantManager.CreateGrant(context.TODO(), grant, publicKey) + err = grantManager.CreateGrant(ctx, grant, publicKey) require.NoError(t, err) // All three get methods should only return the public key when using the valid subject - _, err = grantStorage.GetPublicKey(context.TODO(), issuer, "any-subject-1", publicKey.KeyID) + _, err = grantStorage.GetPublicKey(ctx, issuer, "any-subject-1", publicKey.KeyID) require.Error(t, err) - _, err = grantStorage.GetPublicKey(context.TODO(), issuer, subject, publicKey.KeyID) + _, err = grantStorage.GetPublicKey(ctx, issuer, subject, publicKey.KeyID) require.NoError(t, err) - _, err = grantStorage.GetPublicKeyScopes(context.TODO(), issuer, "any-subject-2", publicKey.KeyID) + _, err = grantStorage.GetPublicKeyScopes(ctx, issuer, "any-subject-2", publicKey.KeyID) require.Error(t, err) - _, err = grantStorage.GetPublicKeyScopes(context.TODO(), issuer, subject, publicKey.KeyID) + _, err = grantStorage.GetPublicKeyScopes(ctx, issuer, subject, publicKey.KeyID) require.NoError(t, err) - jwks, err := grantStorage.GetPublicKeys(context.TODO(), issuer, "any-subject-3") + jwks, err := grantStorage.GetPublicKeys(ctx, issuer, "any-subject-3") require.NoError(t, err) require.NotNil(t, jwks) require.Empty(t, jwks.Keys) - jwks, err = grantStorage.GetPublicKeys(context.TODO(), issuer, subject) + jwks, err = grantStorage.GetPublicKeys(ctx, issuer, subject) require.NoError(t, err) require.NotNil(t, jwks) require.NotEmpty(t, jwks.Keys) @@ -1126,17 +1187,17 @@ func testFositeJWTBearerGrantStorage(x InternalRegistry) func(t *testing.T) { ExpiresAt: time.Now().UTC().Round(time.Second).AddDate(1, 0, 0), } - err = grantManager.CreateGrant(context.TODO(), grant, publicKey) + err = grantManager.CreateGrant(ctx, grant, publicKey) require.NoError(t, err) // All three get methods should always return the public key - _, err = grantStorage.GetPublicKey(context.TODO(), issuer, "any-subject-1", publicKey.KeyID) + _, err = grantStorage.GetPublicKey(ctx, issuer, "any-subject-1", publicKey.KeyID) require.NoError(t, err) - _, err = grantStorage.GetPublicKeyScopes(context.TODO(), issuer, "any-subject-2", publicKey.KeyID) + _, err = grantStorage.GetPublicKeyScopes(ctx, issuer, "any-subject-2", publicKey.KeyID) require.NoError(t, err) - jwks, err := grantStorage.GetPublicKeys(context.TODO(), issuer, "any-subject-3") + jwks, err := grantStorage.GetPublicKeys(ctx, issuer, "any-subject-3") require.NoError(t, err) require.NotNil(t, jwks) require.NotEmpty(t, jwks.Keys) @@ -1159,10 +1220,10 @@ func testFositeJWTBearerGrantStorage(x InternalRegistry) func(t *testing.T) { ExpiresAt: time.Now().UTC().Round(time.Second).AddDate(-1, 0, 0), } - err = grantManager.CreateGrant(context.TODO(), grant, publicKey) + err = grantManager.CreateGrant(ctx, grant, publicKey) require.NoError(t, err) - keys, err := grantStorage.GetPublicKeys(context.TODO(), issuer, "any-subject-3") + keys, err := grantStorage.GetPublicKeys(ctx, issuer, "any-subject-3") require.NoError(t, err) assert.Len(t, keys.Keys, 0) }) diff --git a/oauth2/handler.go b/oauth2/handler.go index 960433c3c86..288ed1f16f0 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -497,7 +497,7 @@ func (h *Handler) discoverOidcConfiguration(w http.ResponseWriter, r *http.Reque IDTokenSignedResponseAlg: []string{key.Algorithm}, UserinfoSignedResponseAlg: []string{key.Algorithm}, GrantTypesSupported: []string{"authorization_code", "implicit", "client_credentials", "refresh_token"}, - ResponseModesSupported: []string{"query", "fragment"}, + ResponseModesSupported: []string{"query", "fragment", "form_post"}, UserinfoSigningAlgValuesSupported: []string{"none", key.Algorithm}, RequestParameterSupported: true, RequestURIParameterSupported: true, @@ -962,7 +962,8 @@ func (h *Handler) oauth2TokenExchange(w http.ResponseWriter, r *http.Request) { } if accessRequest.GetGrantTypes().ExactOne(string(fosite.GrantTypeClientCredentials)) || - accessRequest.GetGrantTypes().ExactOne(string(fosite.GrantTypeJWTBearer)) { + accessRequest.GetGrantTypes().ExactOne(string(fosite.GrantTypeJWTBearer)) || + accessRequest.GetGrantTypes().ExactOne(string(fosite.GrantTypePassword)) { var accessTokenKeyID string if h.c.AccessTokenStrategy(ctx, client.AccessTokenStrategySource(accessRequest.GetClient())) == "jwt" { accessTokenKeyID, err = h.r.AccessTokenJWTStrategy().GetPublicKeyID(ctx) @@ -975,9 +976,21 @@ func (h *Handler) oauth2TokenExchange(w http.ResponseWriter, r *http.Request) { } // only for client_credentials, otherwise Authentication is included in session - if accessRequest.GetGrantTypes().ExactOne("client_credentials") { + if accessRequest.GetGrantTypes().ExactOne(string(fosite.GrantTypeClientCredentials)) { session.Subject = accessRequest.GetClient().GetID() } + // only for password grant, otherwise Authentication is included in session + if accessRequest.GetGrantTypes().ExactOne(string(fosite.GrantTypePassword)) { + if sess, ok := accessRequest.GetSession().(fosite.ExtraClaimsSession); ok { + sess.GetExtraClaims()["username"] = accessRequest.GetRequestForm().Get("username") + session.DefaultSession.Username = accessRequest.GetRequestForm().Get("username") + } + + // Also add audience claims + for _, aud := range accessRequest.GetClient().GetAudience() { + accessRequest.GrantAudience(aud) + } + } session.ClientID = accessRequest.GetClient().GetID() session.KID = accessTokenKeyID session.DefaultSession.Claims.Issuer = h.c.IssuerURL(ctx).String() diff --git a/oauth2/oauth2_auth_code_test.go b/oauth2/oauth2_auth_code_test.go index aa6934062ed..0d89e14ac9b 100644 --- a/oauth2/oauth2_auth_code_test.go +++ b/oauth2/oauth2_auth_code_test.go @@ -165,7 +165,7 @@ func TestAuthCodeWithDefaultStrategy(t *testing.T) { GrantAccessTokenAudience: rr.RequestedAccessTokenAudience, Session: &hydra.AcceptOAuth2ConsentRequestSession{ AccessToken: map[string]interface{}{"foo": "bar"}, - IdToken: map[string]interface{}{"bar": "baz"}, + IdToken: map[string]interface{}{"bar": "baz", "email": "foo@bar.com"}, }, }). Execute() @@ -176,8 +176,9 @@ func TestAuthCodeWithDefaultStrategy(t *testing.T) { } assertRefreshToken := func(t *testing.T, token *oauth2.Token, c *oauth2.Config, expectedExp time.Time) { - actualExp, err := strconv.ParseInt(testhelpers.IntrospectToken(t, c, token.RefreshToken, adminTS).Get("exp").String(), 10, 64) - require.NoError(t, err) + introspect := testhelpers.IntrospectToken(t, c, token.RefreshToken, adminTS) + actualExp, err := strconv.ParseInt(introspect.Get("exp").String(), 10, 64) + require.NoError(t, err, "%s", introspect) requirex.EqualTime(t, expectedExp, time.Unix(actualExp, 0), time.Second) } @@ -206,6 +207,8 @@ func TestAuthCodeWithDefaultStrategy(t *testing.T) { assert.EqualValues(t, expectedSubject, claims.Get("sub").String(), "%s", claims) assert.EqualValues(t, expectedNonce, claims.Get("nonce").String(), "%s", claims) assert.EqualValues(t, `baz`, claims.Get("bar").String(), "%s", claims) + assert.EqualValues(t, `foo@bar.com`, claims.Get("email").String(), "%s", claims) + assert.NotEmpty(t, claims.Get("sid").String(), "%s", claims) return claims } @@ -330,6 +333,186 @@ func TestAuthCodeWithDefaultStrategy(t *testing.T) { }) }) + t.Run("case=graceful token rotation", func(t *testing.T) { + run := func(t *testing.T, strategy string) { + reg.Config().MustSet(ctx, config.KeyRefreshTokenRotationGracePeriod, "5s") + t.Cleanup(func() { + reg.Config().MustSet(ctx, config.KeyRefreshTokenRotationGracePeriod, nil) + }) + + c, conf := newOAuth2Client(t, reg, testhelpers.NewCallbackURL(t, "callback", testhelpers.HTTPServerNotImplementedHandler)) + testhelpers.NewLoginConsentUI(t, reg.Config(), + acceptLoginHandler(t, c, subject, nil), + acceptConsentHandler(t, c, subject, nil), + ) + + issueTokens := func(t *testing.T) *oauth2.Token { + code, _ := getAuthorizeCode(t, conf, nil, oauth2.SetAuthURLParam("nonce", nonce)) + require.NotEmpty(t, code) + token, err := conf.Exchange(context.Background(), code) + iat := time.Now() + require.NoError(t, err) + + introspectAccessToken(t, conf, token, subject) + assertJWTAccessToken(t, strategy, conf, token, subject, iat.Add(reg.Config().GetAccessTokenLifespan(ctx)), `["hydra","offline","openid"]`) + assertIDToken(t, token, conf, subject, nonce, iat.Add(reg.Config().GetIDTokenLifespan(ctx))) + assertRefreshToken(t, token, conf, iat.Add(reg.Config().GetRefreshTokenLifespan(ctx))) + return token + } + + refreshTokens := func(t *testing.T, token *oauth2.Token) *oauth2.Token { + require.NotEmpty(t, token.RefreshToken) + token.Expiry = token.Expiry.Add(-time.Hour * 24) + iat := time.Now() + refreshedToken, err := conf.TokenSource(context.Background(), token).Token() + require.NoError(t, err) + + require.NotEqual(t, token.AccessToken, refreshedToken.AccessToken) + require.NotEqual(t, token.RefreshToken, refreshedToken.RefreshToken) + require.NotEqual(t, token.Extra("id_token"), refreshedToken.Extra("id_token")) + + introspectAccessToken(t, conf, refreshedToken, subject) + assertJWTAccessToken(t, strategy, conf, refreshedToken, subject, iat.Add(reg.Config().GetAccessTokenLifespan(ctx)), `["hydra","offline","openid"]`) + assertIDToken(t, refreshedToken, conf, subject, nonce, iat.Add(reg.Config().GetIDTokenLifespan(ctx))) + assertRefreshToken(t, refreshedToken, conf, iat.Add(reg.Config().GetRefreshTokenLifespan(ctx))) + return refreshedToken + } + + t.Run("followup=successfully perform refresh token flow", func(t *testing.T) { + start := time.Now() + + token := issueTokens(t) + var first, second *oauth2.Token + t.Run("followup=first refresh", func(t *testing.T) { + first = refreshTokens(t, token) + }) + + t.Run("followup=second refresh", func(t *testing.T) { + second = refreshTokens(t, token) + }) + + // Sleep until the grace period is over + time.Sleep(time.Until(start.Add(5*time.Second + time.Millisecond*10))) + t.Run("followup=refresh failure invalidates all tokens", func(t *testing.T) { + _, err := conf.TokenSource(context.Background(), token).Token() + assert.Error(t, err) + + i := testhelpers.IntrospectToken(t, conf, first.AccessToken, adminTS) + assert.False(t, i.Get("active").Bool(), "%s", i) + + i = testhelpers.IntrospectToken(t, conf, second.AccessToken, adminTS) + assert.False(t, i.Get("active").Bool(), "%s", i) + + i = testhelpers.IntrospectToken(t, conf, first.RefreshToken, adminTS) + assert.False(t, i.Get("active").Bool(), "%s", i) + + i = testhelpers.IntrospectToken(t, conf, second.RefreshToken, adminTS) + assert.False(t, i.Get("active").Bool(), "%s", i) + }) + }) + + t.Run("followup=successfully perform refresh token flow", func(t *testing.T) { + start := time.Now() + + token := issueTokens(t) + var first, second *oauth2.Token + t.Run("followup=first refresh", func(t *testing.T) { + first = refreshTokens(t, token) + }) + + t.Run("followup=second refresh", func(t *testing.T) { + second = refreshTokens(t, token) + }) + + // Sleep until the grace period is over + time.Sleep(time.Until(start.Add(5*time.Second + time.Millisecond*10))) + t.Run("followup=revoking consent revokes all tokens", func(t *testing.T) { + err := reg.ConsentManager().RevokeSubjectConsentSession(context.Background(), subject) + require.NoError(t, err) + + _, err = conf.TokenSource(context.Background(), token).Token() + assert.Error(t, err) + + i := testhelpers.IntrospectToken(t, conf, first.AccessToken, adminTS) + assert.False(t, i.Get("active").Bool(), "%s", i) + + i = testhelpers.IntrospectToken(t, conf, second.AccessToken, adminTS) + assert.False(t, i.Get("active").Bool(), "%s", i) + + i = testhelpers.IntrospectToken(t, conf, first.RefreshToken, adminTS) + assert.False(t, i.Get("active").Bool(), "%s", i) + + i = testhelpers.IntrospectToken(t, conf, second.RefreshToken, adminTS) + assert.False(t, i.Get("active").Bool(), "%s", i) + }) + }) + + t.Run("followup=graceful refresh tokens are all refreshed", func(t *testing.T) { + start := time.Now() + token := issueTokens(t) + var a1Refresh, b1Refresh, a2RefreshA, a2RefreshB, b2RefreshA, b2RefreshB *oauth2.Token + t.Run("followup=first refresh", func(t *testing.T) { + a1Refresh = refreshTokens(t, token) + }) + + t.Run("followup=second refresh", func(t *testing.T) { + b1Refresh = refreshTokens(t, token) + }) + + t.Run("followup=first refresh from first refresh", func(t *testing.T) { + a2RefreshA = refreshTokens(t, a1Refresh) + }) + + t.Run("followup=second refresh from first refresh", func(t *testing.T) { + a2RefreshB = refreshTokens(t, a1Refresh) + }) + + t.Run("followup=first refresh from second refresh", func(t *testing.T) { + b2RefreshA = refreshTokens(t, b1Refresh) + }) + + t.Run("followup=second refresh from second refresh", func(t *testing.T) { + b2RefreshB = refreshTokens(t, b1Refresh) + }) + + // Sleep until the grace period is over + time.Sleep(time.Until(start.Add(5*time.Second + time.Millisecond*10))) + t.Run("followup=refresh failure invalidates all tokens", func(t *testing.T) { + _, err := conf.TokenSource(context.Background(), token).Token() + assert.Error(t, err) + + for k, token := range []*oauth2.Token{ + a1Refresh, b1Refresh, a2RefreshA, a2RefreshB, b2RefreshA, b2RefreshB, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + i := testhelpers.IntrospectToken(t, conf, token.AccessToken, adminTS) + assert.False(t, i.Get("active").Bool(), "%s", i) + + i = testhelpers.IntrospectToken(t, conf, token.AccessToken, adminTS) + assert.False(t, i.Get("active").Bool(), "%s", i) + + i = testhelpers.IntrospectToken(t, conf, token.RefreshToken, adminTS) + assert.False(t, i.Get("active").Bool(), "%s", i) + + i = testhelpers.IntrospectToken(t, conf, token.RefreshToken, adminTS) + assert.False(t, i.Get("active").Bool(), "%s", i) + }) + } + }) + }) + } + + t.Run("strategy=jwt", func(t *testing.T) { + reg.Config().MustSet(ctx, config.KeyAccessTokenStrategy, "jwt") + run(t, "jwt") + }) + + t.Run("strategy=opaque", func(t *testing.T) { + reg.Config().MustSet(ctx, config.KeyAccessTokenStrategy, "opaque") + run(t, "opaque") + }) + }) + t.Run("case=perform authorize code flow with verifable credentials", func(t *testing.T) { // Make sure we test against all crypto suites that we advertise. cfg, _, err := publicClient.OidcAPI.DiscoverOidcConfiguration(ctx).Execute() diff --git a/oauth2/oauth2_rop_test.go b/oauth2/oauth2_rop_test.go index 73946a25539..4adb4904452 100644 --- a/oauth2/oauth2_rop_test.go +++ b/oauth2/oauth2_rop_test.go @@ -5,7 +5,11 @@ package oauth2_test import ( "context" + "encoding/json" + "net/http" + "net/http/httptest" "testing" + "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -13,12 +17,18 @@ import ( "golang.org/x/oauth2" "github.com/ory/fosite/compose" + "github.com/ory/fosite/token/jwt" hydra "github.com/ory/hydra/v2/client" + "github.com/ory/hydra/v2/driver/config" + "github.com/ory/hydra/v2/flow" "github.com/ory/hydra/v2/fositex" "github.com/ory/hydra/v2/internal" "github.com/ory/hydra/v2/internal/kratos" "github.com/ory/hydra/v2/internal/testhelpers" + hydraoauth2 "github.com/ory/hydra/v2/oauth2" + "github.com/ory/hydra/v2/x" "github.com/ory/x/contextx" + "github.com/ory/x/sqlxx" ) func TestResourceOwnerPasswordGrant(t *testing.T) { @@ -27,12 +37,19 @@ func TestResourceOwnerPasswordGrant(t *testing.T) { reg := internal.NewMockedRegistry(t, &contextx.Default{}) reg.WithKratos(fakeKratos) reg.WithExtraFositeFactories([]fositex.Factory{compose.OAuth2ResourceOwnerPasswordCredentialsFactory}) - _, adminTS := testhelpers.NewOAuth2Server(ctx, t, reg) + publicTS, adminTS := testhelpers.NewOAuth2Server(ctx, t, reg) secret := uuid.New().String() + audience := sqlxx.StringSliceJSONFormat{"https://aud.example.com"} client := &hydra.Client{ Secret: secret, - GrantTypes: []string{"password"}, + GrantTypes: []string{"password", "refresh_token"}, + Scope: "offline", + Audience: audience, + Lifespans: hydra.Lifespans{ + PasswordGrantAccessTokenLifespan: x.NullDuration{Duration: 1 * time.Hour, Valid: true}, + PasswordGrantRefreshTokenLifespan: x.NullDuration{Duration: 1 * time.Hour, Valid: true}, + }, } require.NoError(t, reg.ClientManager().CreateClient(ctx, client)) @@ -44,15 +61,94 @@ func TestResourceOwnerPasswordGrant(t *testing.T) { TokenURL: reg.Config().OAuth2TokenURL(ctx).String(), AuthStyle: oauth2.AuthStyleInHeader, }, + Scopes: []string{"offline"}, } + hs := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Header.Get("Content-Type"), "application/json; charset=UTF-8") + assert.Equal(t, r.Header.Get("Authorization"), "Bearer secret value") + + var hookReq hydraoauth2.TokenHookRequest + require.NoError(t, json.NewDecoder(r.Body).Decode(&hookReq)) + assert.NotEmpty(t, hookReq.Session) + assert.NotEmpty(t, hookReq.Request) + + claims := hookReq.Session.Extra + claims["hooked"] = true + if hookReq.Request.GrantTypes[0] == "refresh_token" { + claims["refreshed"] = true + } + + hookResp := hydraoauth2.TokenHookResponse{ + Session: flow.AcceptOAuth2ConsentRequestSession{ + AccessToken: claims, + IDToken: claims, + }, + } + + w.WriteHeader(http.StatusOK) + require.NoError(t, json.NewEncoder(w).Encode(&hookResp)) + })) + defer hs.Close() + + reg.Config().MustSet(ctx, config.KeyTokenHook, &config.HookConfig{ + URL: hs.URL, + Auth: &config.Auth{ + Type: "api_key", + Config: config.AuthConfig{ + In: "header", + Name: "Authorization", + Value: "Bearer secret value", + }, + }, + }) + reg.Config().MustSet(ctx, config.KeyAccessTokenStrategy, "jwt") + t.Run("case=get ROP grant token with valid username and password", func(t *testing.T) { token, err := oauth2Config.PasswordCredentialsToken(ctx, kratos.FakeUsername, kratos.FakePassword) require.NoError(t, err) require.NotEmpty(t, token.AccessToken) - i := testhelpers.IntrospectToken(t, oauth2Config, token.AccessToken, adminTS) - assert.True(t, i.Get("active").Bool(), "%s", i) - assert.EqualValues(t, oauth2Config.ClientID, i.Get("client_id").String(), "%s", i) + + // Access token should have hook and identity_id claims + jwtAT, err := jwt.Parse(token.AccessToken, func(token *jwt.Token) (interface{}, error) { + return reg.AccessTokenJWTStrategy().GetPublicKey(ctx) + }) + require.NoError(t, err) + assert.Equal(t, kratos.FakeUsername, jwtAT.Claims["ext"].(map[string]any)["username"]) + assert.Equal(t, kratos.FakeIdentityID, jwtAT.Claims["sub"]) + assert.Equal(t, publicTS.URL, jwtAT.Claims["iss"]) + assert.True(t, jwtAT.Claims["ext"].(map[string]any)["hooked"].(bool)) + assert.ElementsMatch(t, audience, jwtAT.Claims["aud"]) + + t.Run("case=introspect token", func(t *testing.T) { + // Introspected token should have hook and identity_id claims + i := testhelpers.IntrospectToken(t, oauth2Config, token.AccessToken, adminTS) + assert.True(t, i.Get("active").Bool(), "%s", i) + assert.Equal(t, kratos.FakeUsername, i.Get("ext.username").String(), "%s", i) + assert.Equal(t, kratos.FakeIdentityID, i.Get("sub").String(), "%s", i) + assert.True(t, i.Get("ext.hooked").Bool(), "%s", i) + assert.EqualValues(t, oauth2Config.ClientID, i.Get("client_id").String(), "%s", i) + }) + + t.Run("case=refresh token", func(t *testing.T) { + // Refreshed access token should have hook and identity_id claims + require.NotEmpty(t, token.RefreshToken) + token.Expiry = token.Expiry.Add(-time.Hour * 24) + refreshedToken, err := oauth2Config.TokenSource(context.Background(), token).Token() + require.NoError(t, err) + + require.NotEqual(t, token.AccessToken, refreshedToken.AccessToken) + require.NotEqual(t, token.RefreshToken, refreshedToken.RefreshToken) + + jwtAT, err := jwt.Parse(refreshedToken.AccessToken, func(token *jwt.Token) (interface{}, error) { + return reg.AccessTokenJWTStrategy().GetPublicKey(ctx) + }) + require.NoError(t, err) + assert.Equal(t, kratos.FakeIdentityID, jwtAT.Claims["sub"]) + assert.Equal(t, kratos.FakeUsername, jwtAT.Claims["ext"].(map[string]any)["username"]) + assert.True(t, jwtAT.Claims["ext"].(map[string]any)["hooked"].(bool)) + assert.True(t, jwtAT.Claims["ext"].(map[string]any)["refreshed"].(bool)) + }) }) t.Run("case=access denied for invalid password", func(t *testing.T) { diff --git a/oauth2/session.go b/oauth2/session.go index 6dc9039eb22..0630cb09142 100644 --- a/oauth2/session.go +++ b/oauth2/session.go @@ -194,3 +194,17 @@ func (s *Session) UnmarshalJSON(original []byte) (err error) { return nil } + +// GetExtraClaims implements ExtraClaimsSession for Session. +// The returned value can be modified in-place. +func (s *Session) GetExtraClaims() map[string]interface{} { + if s == nil { + return nil + } + + if s.Extra == nil { + s.Extra = make(map[string]interface{}) + } + + return s.Extra +} diff --git a/oauth2/token_hook.go b/oauth2/token_hook.go index d32cadd7e4d..5d5d212e4b8 100644 --- a/oauth2/token_hook.go +++ b/oauth2/token_hook.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "encoding/json" + "io" "net/http" "github.com/pkg/errors" @@ -112,6 +113,7 @@ func executeHookAndUpdateSession(ctx context.Context, reg x.HTTPClientProvider, ) } defer resp.Body.Close() + resp.Body = io.NopCloser(io.LimitReader(resp.Body, 5<<20 /* 5 MiB */)) switch resp.StatusCode { case http.StatusOK: diff --git a/persistence/definitions.go b/persistence/definitions.go index 5cd9c9a3f15..ac9076eb2d3 100644 --- a/persistence/definitions.go +++ b/persistence/definitions.go @@ -34,7 +34,7 @@ type ( PrepareMigration(context.Context) error Connection(context.Context) *pop.Connection Transaction(context.Context, func(ctx context.Context, c *pop.Connection) error) error - Ping() error + Ping(context.Context) error Networker } Provider interface { diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0001.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0001.json index c790dc021e7..fae8513d60a 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0001.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0001.json @@ -1,34 +1,26 @@ { - "ID": "challenge-0001", - "NID": "00000000-0000-0000-0000-000000000000", - "RequestedScope": [ + "i": "challenge-0001", + "n": "00000000-0000-0000-0000-000000000000", + "rs": [ "requested_scope-0001_1" ], - "RequestedAudience": [], - "LoginSkip": true, - "Subject": "subject-0001", - "OpenIDConnectContext": { + "ls": true, + "s": "subject-0001", + "oc": { "display": "display-0001" }, - "Client": null, - "ClientID": "", - "RequestURL": "http://request/0001", - "SessionID": "", - "IdentityProviderSessionID": "", - "LoginVerifier": "verifier-0001", - "LoginCSRF": "csrf-0001", - "LoginInitializedAt": null, - "RequestedAt": "0001-01-01T00:00:00Z", - "State": 128, - "LoginRemember": true, - "LoginRememberFor": 1, - "LoginExtendSessionLifespan": false, - "ACR": "acr-0001", - "AMR": [], - "ForceSubjectIdentifier": "", - "Context": {}, - "LoginWasUsed": true, - "LoginError": { + "r": "http://request/0001", + "lv": "verifier-0001", + "lc": "csrf-0001", + "li": null, + "ia": "0001-01-01T00:00:00Z", + "q": 128, + "lr": true, + "lf": 1, + "a": "acr-0001", + "ct": {}, + "lu": true, + "le": { "error": "", "error_description": "", "error_hint": "", @@ -36,20 +28,19 @@ "error_debug": "", "valid": false }, - "LoginAuthenticatedAt": null, - "ConsentChallengeID": "challenge-0001", - "ConsentSkip": true, - "ConsentVerifier": "verifier-0001", - "ConsentCSRF": "csrf-0001", - "GrantedScope": [ + "la": null, + "cc": "challenge-0001", + "cs": true, + "cv": "verifier-0001", + "cr": "csrf-0001", + "gs": [ "granted_scope-0001_1" ], - "GrantedAudience": [], - "ConsentRemember": true, - "ConsentRememberFor": 1, - "ConsentHandledAt": null, - "ConsentWasHandled": true, - "ConsentError": { + "ce": true, + "cf": 1, + "ch": null, + "cw": true, + "cx": { "error": "", "error_description": "", "error_hint": "", @@ -57,10 +48,10 @@ "error_debug": "", "valid": false }, - "SessionIDToken": { + "st": { "session_id_token-0001": "0001" }, - "SessionAccessToken": { + "sa": { "session_access_token-0001": "0001" } } diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0002.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0002.json index 1917ef94c5f..bc73e23fc21 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0002.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0002.json @@ -1,34 +1,27 @@ { - "ID": "challenge-0002", - "NID": "00000000-0000-0000-0000-000000000000", - "RequestedScope": [ + "i": "challenge-0002", + "n": "00000000-0000-0000-0000-000000000000", + "rs": [ "requested_scope-0002_1" ], - "RequestedAudience": [], - "LoginSkip": true, - "Subject": "subject-0002", - "OpenIDConnectContext": { + "ls": true, + "s": "subject-0002", + "oc": { "display": "display-0002" }, - "Client": null, - "ClientID": "", - "RequestURL": "http://request/0002", - "SessionID": "", - "IdentityProviderSessionID": "", - "LoginVerifier": "verifier-0002", - "LoginCSRF": "csrf-0002", - "LoginInitializedAt": null, - "RequestedAt": "0001-01-01T00:00:00Z", - "State": 128, - "LoginRemember": true, - "LoginRememberFor": 2, - "LoginExtendSessionLifespan": false, - "ACR": "acr-0002", - "AMR": [], - "ForceSubjectIdentifier": "force_subject_id-0002", - "Context": {}, - "LoginWasUsed": true, - "LoginError": { + "r": "http://request/0002", + "lv": "verifier-0002", + "lc": "csrf-0002", + "li": null, + "ia": "0001-01-01T00:00:00Z", + "q": 128, + "lr": true, + "lf": 2, + "a": "acr-0002", + "fs": "force_subject_id-0002", + "ct": {}, + "lu": true, + "le": { "error": "", "error_description": "", "error_hint": "", @@ -36,20 +29,19 @@ "error_debug": "", "valid": false }, - "LoginAuthenticatedAt": null, - "ConsentChallengeID": "challenge-0002", - "ConsentSkip": true, - "ConsentVerifier": "verifier-0002", - "ConsentCSRF": "csrf-0002", - "GrantedScope": [ + "la": null, + "cc": "challenge-0002", + "cs": true, + "cv": "verifier-0002", + "cr": "csrf-0002", + "gs": [ "granted_scope-0002_1" ], - "GrantedAudience": [], - "ConsentRemember": true, - "ConsentRememberFor": 2, - "ConsentHandledAt": null, - "ConsentWasHandled": true, - "ConsentError": { + "ce": true, + "cf": 2, + "ch": null, + "cw": true, + "cx": { "error": "", "error_description": "", "error_hint": "", @@ -57,10 +49,10 @@ "error_debug": "", "valid": false }, - "SessionIDToken": { + "st": { "session_id_token-0002": "0002" }, - "SessionAccessToken": { + "sa": { "session_access_token-0002": "0002" } } diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0003.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0003.json index 39ca512a15a..f04dee37267 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0003.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0003.json @@ -1,34 +1,28 @@ { - "ID": "challenge-0003", - "NID": "00000000-0000-0000-0000-000000000000", - "RequestedScope": [ + "i": "challenge-0003", + "n": "00000000-0000-0000-0000-000000000000", + "rs": [ "requested_scope-0003_1" ], - "RequestedAudience": [], - "LoginSkip": true, - "Subject": "subject-0003", - "OpenIDConnectContext": { + "ls": true, + "s": "subject-0003", + "oc": { "display": "display-0003" }, - "Client": null, - "ClientID": "", - "RequestURL": "http://request/0003", - "SessionID": "auth_session-0003", - "IdentityProviderSessionID": "", - "LoginVerifier": "verifier-0003", - "LoginCSRF": "csrf-0003", - "LoginInitializedAt": null, - "RequestedAt": "0001-01-01T00:00:00Z", - "State": 128, - "LoginRemember": true, - "LoginRememberFor": 3, - "LoginExtendSessionLifespan": false, - "ACR": "acr-0003", - "AMR": [], - "ForceSubjectIdentifier": "force_subject_id-0003", - "Context": {}, - "LoginWasUsed": true, - "LoginError": { + "r": "http://request/0003", + "si": "auth_session-0003", + "lv": "verifier-0003", + "lc": "csrf-0003", + "li": null, + "ia": "0001-01-01T00:00:00Z", + "q": 128, + "lr": true, + "lf": 3, + "a": "acr-0003", + "fs": "force_subject_id-0003", + "ct": {}, + "lu": true, + "le": { "error": "", "error_description": "", "error_hint": "", @@ -36,20 +30,19 @@ "error_debug": "", "valid": false }, - "LoginAuthenticatedAt": null, - "ConsentChallengeID": "challenge-0003", - "ConsentSkip": true, - "ConsentVerifier": "verifier-0003", - "ConsentCSRF": "csrf-0003", - "GrantedScope": [ + "la": null, + "cc": "challenge-0003", + "cs": true, + "cv": "verifier-0003", + "cr": "csrf-0003", + "gs": [ "granted_scope-0003_1" ], - "GrantedAudience": [], - "ConsentRemember": true, - "ConsentRememberFor": 3, - "ConsentHandledAt": null, - "ConsentWasHandled": true, - "ConsentError": { + "ce": true, + "cf": 3, + "ch": null, + "cw": true, + "cx": { "error": "", "error_description": "", "error_hint": "", @@ -57,10 +50,10 @@ "error_debug": "", "valid": false }, - "SessionIDToken": { + "st": { "session_id_token-0003": "0003" }, - "SessionAccessToken": { + "sa": { "session_access_token-0003": "0003" } } diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0004.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0004.json index b3dc1b958e0..e3b5d630dd2 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0004.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0004.json @@ -1,36 +1,31 @@ { - "ID": "challenge-0004", - "NID": "00000000-0000-0000-0000-000000000000", - "RequestedScope": [ + "i": "challenge-0004", + "n": "00000000-0000-0000-0000-000000000000", + "rs": [ "requested_scope-0004_1" ], - "RequestedAudience": [ + "ra": [ "requested_audience-0004_1" ], - "LoginSkip": true, - "Subject": "subject-0004", - "OpenIDConnectContext": { + "ls": true, + "s": "subject-0004", + "oc": { "display": "display-0004" }, - "Client": null, - "ClientID": "", - "RequestURL": "http://request/0004", - "SessionID": "auth_session-0004", - "IdentityProviderSessionID": "", - "LoginVerifier": "verifier-0004", - "LoginCSRF": "csrf-0004", - "LoginInitializedAt": null, - "RequestedAt": "0001-01-01T00:00:00Z", - "State": 128, - "LoginRemember": true, - "LoginRememberFor": 4, - "LoginExtendSessionLifespan": false, - "ACR": "acr-0004", - "AMR": [], - "ForceSubjectIdentifier": "force_subject_id-0004", - "Context": {}, - "LoginWasUsed": true, - "LoginError": { + "r": "http://request/0004", + "si": "auth_session-0004", + "lv": "verifier-0004", + "lc": "csrf-0004", + "li": null, + "ia": "0001-01-01T00:00:00Z", + "q": 128, + "lr": true, + "lf": 4, + "a": "acr-0004", + "fs": "force_subject_id-0004", + "ct": {}, + "lu": true, + "le": { "error": "", "error_description": "", "error_hint": "", @@ -38,22 +33,22 @@ "error_debug": "", "valid": false }, - "LoginAuthenticatedAt": null, - "ConsentChallengeID": "challenge-0004", - "ConsentSkip": true, - "ConsentVerifier": "verifier-0004", - "ConsentCSRF": "csrf-0004", - "GrantedScope": [ + "la": null, + "cc": "challenge-0004", + "cs": true, + "cv": "verifier-0004", + "cr": "csrf-0004", + "gs": [ "granted_scope-0004_1" ], - "GrantedAudience": [ + "ga": [ "granted_audience-0004_1" ], - "ConsentRemember": true, - "ConsentRememberFor": 4, - "ConsentHandledAt": null, - "ConsentWasHandled": true, - "ConsentError": { + "ce": true, + "cf": 4, + "ch": null, + "cw": true, + "cx": { "error": "", "error_description": "", "error_hint": "", @@ -61,10 +56,10 @@ "error_debug": "", "valid": false }, - "SessionIDToken": { + "st": { "session_id_token-0004": "0004" }, - "SessionAccessToken": { + "sa": { "session_access_token-0004": "0004" } } diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0005.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0005.json index 2a642cae89b..db4e0787291 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0005.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0005.json @@ -1,36 +1,31 @@ { - "ID": "challenge-0005", - "NID": "00000000-0000-0000-0000-000000000000", - "RequestedScope": [ + "i": "challenge-0005", + "n": "00000000-0000-0000-0000-000000000000", + "rs": [ "requested_scope-0005_1" ], - "RequestedAudience": [ + "ra": [ "requested_audience-0005_1" ], - "LoginSkip": true, - "Subject": "subject-0005", - "OpenIDConnectContext": { + "ls": true, + "s": "subject-0005", + "oc": { "display": "display-0005" }, - "Client": null, - "ClientID": "", - "RequestURL": "http://request/0005", - "SessionID": "auth_session-0005", - "IdentityProviderSessionID": "", - "LoginVerifier": "verifier-0005", - "LoginCSRF": "csrf-0005", - "LoginInitializedAt": null, - "RequestedAt": "0001-01-01T00:00:00Z", - "State": 128, - "LoginRemember": true, - "LoginRememberFor": 5, - "LoginExtendSessionLifespan": false, - "ACR": "acr-0005", - "AMR": [], - "ForceSubjectIdentifier": "force_subject_id-0005", - "Context": {}, - "LoginWasUsed": true, - "LoginError": { + "r": "http://request/0005", + "si": "auth_session-0005", + "lv": "verifier-0005", + "lc": "csrf-0005", + "li": null, + "ia": "0001-01-01T00:00:00Z", + "q": 128, + "lr": true, + "lf": 5, + "a": "acr-0005", + "fs": "force_subject_id-0005", + "ct": {}, + "lu": true, + "le": { "error": "", "error_description": "", "error_hint": "", @@ -38,22 +33,22 @@ "error_debug": "", "valid": false }, - "LoginAuthenticatedAt": null, - "ConsentChallengeID": "challenge-0005", - "ConsentSkip": true, - "ConsentVerifier": "verifier-0005", - "ConsentCSRF": "csrf-0005", - "GrantedScope": [ + "la": null, + "cc": "challenge-0005", + "cs": true, + "cv": "verifier-0005", + "cr": "csrf-0005", + "gs": [ "granted_scope-0005_1" ], - "GrantedAudience": [ + "ga": [ "granted_audience-0005_1" ], - "ConsentRemember": true, - "ConsentRememberFor": 5, - "ConsentHandledAt": null, - "ConsentWasHandled": true, - "ConsentError": { + "ce": true, + "cf": 5, + "ch": null, + "cw": true, + "cx": { "error": "", "error_description": "", "error_hint": "", @@ -61,10 +56,10 @@ "error_debug": "", "valid": false }, - "SessionIDToken": { + "st": { "session_id_token-0005": "0005" }, - "SessionAccessToken": { + "sa": { "session_access_token-0005": "0005" } } diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0006.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0006.json index 89bfd52e6e3..7a8b9fd8890 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0006.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0006.json @@ -1,36 +1,31 @@ { - "ID": "challenge-0006", - "NID": "00000000-0000-0000-0000-000000000000", - "RequestedScope": [ + "i": "challenge-0006", + "n": "00000000-0000-0000-0000-000000000000", + "rs": [ "requested_scope-0006_1" ], - "RequestedAudience": [ + "ra": [ "requested_audience-0006_1" ], - "LoginSkip": true, - "Subject": "subject-0006", - "OpenIDConnectContext": { + "ls": true, + "s": "subject-0006", + "oc": { "display": "display-0006" }, - "Client": null, - "ClientID": "", - "RequestURL": "http://request/0006", - "SessionID": "auth_session-0006", - "IdentityProviderSessionID": "", - "LoginVerifier": "verifier-0006", - "LoginCSRF": "csrf-0006", - "LoginInitializedAt": null, - "RequestedAt": "0001-01-01T00:00:00Z", - "State": 128, - "LoginRemember": true, - "LoginRememberFor": 6, - "LoginExtendSessionLifespan": false, - "ACR": "acr-0006", - "AMR": [], - "ForceSubjectIdentifier": "force_subject_id-0006", - "Context": {}, - "LoginWasUsed": true, - "LoginError": { + "r": "http://request/0006", + "si": "auth_session-0006", + "lv": "verifier-0006", + "lc": "csrf-0006", + "li": null, + "ia": "0001-01-01T00:00:00Z", + "q": 128, + "lr": true, + "lf": 6, + "a": "acr-0006", + "fs": "force_subject_id-0006", + "ct": {}, + "lu": true, + "le": { "error": "", "error_description": "", "error_hint": "", @@ -38,22 +33,22 @@ "error_debug": "", "valid": false }, - "LoginAuthenticatedAt": null, - "ConsentChallengeID": "challenge-0006", - "ConsentSkip": true, - "ConsentVerifier": "verifier-0006", - "ConsentCSRF": "csrf-0006", - "GrantedScope": [ + "la": null, + "cc": "challenge-0006", + "cs": true, + "cv": "verifier-0006", + "cr": "csrf-0006", + "gs": [ "granted_scope-0006_1" ], - "GrantedAudience": [ + "ga": [ "granted_audience-0006_1" ], - "ConsentRemember": true, - "ConsentRememberFor": 6, - "ConsentHandledAt": null, - "ConsentWasHandled": true, - "ConsentError": { + "ce": true, + "cf": 6, + "ch": null, + "cw": true, + "cx": { "error": "", "error_description": "", "error_hint": "", @@ -61,10 +56,10 @@ "error_debug": "", "valid": false }, - "SessionIDToken": { + "st": { "session_id_token-0006": "0006" }, - "SessionAccessToken": { + "sa": { "session_access_token-0006": "0006" } } diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0007.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0007.json index 2c84d09ad47..b5f6814ea47 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0007.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0007.json @@ -1,36 +1,31 @@ { - "ID": "challenge-0007", - "NID": "00000000-0000-0000-0000-000000000000", - "RequestedScope": [ + "i": "challenge-0007", + "n": "00000000-0000-0000-0000-000000000000", + "rs": [ "requested_scope-0007_1" ], - "RequestedAudience": [ + "ra": [ "requested_audience-0007_1" ], - "LoginSkip": true, - "Subject": "subject-0007", - "OpenIDConnectContext": { + "ls": true, + "s": "subject-0007", + "oc": { "display": "display-0007" }, - "Client": null, - "ClientID": "", - "RequestURL": "http://request/0007", - "SessionID": "auth_session-0007", - "IdentityProviderSessionID": "", - "LoginVerifier": "verifier-0007", - "LoginCSRF": "csrf-0007", - "LoginInitializedAt": null, - "RequestedAt": "0001-01-01T00:00:00Z", - "State": 128, - "LoginRemember": true, - "LoginRememberFor": 7, - "LoginExtendSessionLifespan": false, - "ACR": "acr-0007", - "AMR": [], - "ForceSubjectIdentifier": "force_subject_id-0007", - "Context": {}, - "LoginWasUsed": true, - "LoginError": { + "r": "http://request/0007", + "si": "auth_session-0007", + "lv": "verifier-0007", + "lc": "csrf-0007", + "li": null, + "ia": "0001-01-01T00:00:00Z", + "q": 128, + "lr": true, + "lf": 7, + "a": "acr-0007", + "fs": "force_subject_id-0007", + "ct": {}, + "lu": true, + "le": { "error": "", "error_description": "", "error_hint": "", @@ -38,22 +33,22 @@ "error_debug": "", "valid": false }, - "LoginAuthenticatedAt": null, - "ConsentChallengeID": "challenge-0007", - "ConsentSkip": true, - "ConsentVerifier": "verifier-0007", - "ConsentCSRF": "csrf-0007", - "GrantedScope": [ + "la": null, + "cc": "challenge-0007", + "cs": true, + "cv": "verifier-0007", + "cr": "csrf-0007", + "gs": [ "granted_scope-0007_1" ], - "GrantedAudience": [ + "ga": [ "granted_audience-0007_1" ], - "ConsentRemember": true, - "ConsentRememberFor": 7, - "ConsentHandledAt": null, - "ConsentWasHandled": true, - "ConsentError": { + "ce": true, + "cf": 7, + "ch": null, + "cw": true, + "cx": { "error": "", "error_description": "", "error_hint": "", @@ -61,10 +56,10 @@ "error_debug": "", "valid": false }, - "SessionIDToken": { + "st": { "session_id_token-0007": "0007" }, - "SessionAccessToken": { + "sa": { "session_access_token-0007": "0007" } } diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0008.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0008.json index d2e7d2f2fdd..e821518707f 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0008.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0008.json @@ -1,38 +1,33 @@ { - "ID": "challenge-0008", - "NID": "00000000-0000-0000-0000-000000000000", - "RequestedScope": [ + "i": "challenge-0008", + "n": "00000000-0000-0000-0000-000000000000", + "rs": [ "requested_scope-0008_1" ], - "RequestedAudience": [ + "ra": [ "requested_audience-0008_1" ], - "LoginSkip": true, - "Subject": "subject-0008", - "OpenIDConnectContext": { + "ls": true, + "s": "subject-0008", + "oc": { "display": "display-0008" }, - "Client": null, - "ClientID": "", - "RequestURL": "http://request/0008", - "SessionID": "auth_session-0008", - "IdentityProviderSessionID": "", - "LoginVerifier": "verifier-0008", - "LoginCSRF": "csrf-0008", - "LoginInitializedAt": null, - "RequestedAt": "0001-01-01T00:00:00Z", - "State": 128, - "LoginRemember": true, - "LoginRememberFor": 8, - "LoginExtendSessionLifespan": false, - "ACR": "acr-0008", - "AMR": [], - "ForceSubjectIdentifier": "force_subject_id-0008", - "Context": { + "r": "http://request/0008", + "si": "auth_session-0008", + "lv": "verifier-0008", + "lc": "csrf-0008", + "li": null, + "ia": "0001-01-01T00:00:00Z", + "q": 128, + "lr": true, + "lf": 8, + "a": "acr-0008", + "fs": "force_subject_id-0008", + "ct": { "context": "0008" }, - "LoginWasUsed": true, - "LoginError": { + "lu": true, + "le": { "error": "", "error_description": "", "error_hint": "", @@ -40,22 +35,22 @@ "error_debug": "", "valid": false }, - "LoginAuthenticatedAt": null, - "ConsentChallengeID": "challenge-0008", - "ConsentSkip": true, - "ConsentVerifier": "verifier-0008", - "ConsentCSRF": "csrf-0008", - "GrantedScope": [ + "la": null, + "cc": "challenge-0008", + "cs": true, + "cv": "verifier-0008", + "cr": "csrf-0008", + "gs": [ "granted_scope-0008_1" ], - "GrantedAudience": [ + "ga": [ "granted_audience-0008_1" ], - "ConsentRemember": true, - "ConsentRememberFor": 8, - "ConsentHandledAt": null, - "ConsentWasHandled": true, - "ConsentError": { + "ce": true, + "cf": 8, + "ch": null, + "cw": true, + "cx": { "error": "", "error_description": "", "error_hint": "", @@ -63,10 +58,10 @@ "error_debug": "", "valid": false }, - "SessionIDToken": { + "st": { "session_id_token-0008": "0008" }, - "SessionAccessToken": { + "sa": { "session_access_token-0008": "0008" } } diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0009.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0009.json index 6d3e70d5e37..be51195ca6a 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0009.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0009.json @@ -1,38 +1,33 @@ { - "ID": "challenge-0009", - "NID": "00000000-0000-0000-0000-000000000000", - "RequestedScope": [ + "i": "challenge-0009", + "n": "00000000-0000-0000-0000-000000000000", + "rs": [ "requested_scope-0009_1" ], - "RequestedAudience": [ + "ra": [ "requested_audience-0009_1" ], - "LoginSkip": true, - "Subject": "subject-0009", - "OpenIDConnectContext": { + "ls": true, + "s": "subject-0009", + "oc": { "display": "display-0009" }, - "Client": null, - "ClientID": "", - "RequestURL": "http://request/0009", - "SessionID": "auth_session-0009", - "IdentityProviderSessionID": "", - "LoginVerifier": "verifier-0009", - "LoginCSRF": "csrf-0009", - "LoginInitializedAt": null, - "RequestedAt": "0001-01-01T00:00:00Z", - "State": 128, - "LoginRemember": true, - "LoginRememberFor": 9, - "LoginExtendSessionLifespan": false, - "ACR": "acr-0009", - "AMR": [], - "ForceSubjectIdentifier": "force_subject_id-0009", - "Context": { + "r": "http://request/0009", + "si": "auth_session-0009", + "lv": "verifier-0009", + "lc": "csrf-0009", + "li": null, + "ia": "0001-01-01T00:00:00Z", + "q": 128, + "lr": true, + "lf": 9, + "a": "acr-0009", + "fs": "force_subject_id-0009", + "ct": { "context": "0009" }, - "LoginWasUsed": true, - "LoginError": { + "lu": true, + "le": { "error": "", "error_description": "", "error_hint": "", @@ -40,22 +35,22 @@ "error_debug": "", "valid": false }, - "LoginAuthenticatedAt": null, - "ConsentChallengeID": "challenge-0009", - "ConsentSkip": true, - "ConsentVerifier": "verifier-0009", - "ConsentCSRF": "csrf-0009", - "GrantedScope": [ + "la": null, + "cc": "challenge-0009", + "cs": true, + "cv": "verifier-0009", + "cr": "csrf-0009", + "gs": [ "granted_scope-0009_1" ], - "GrantedAudience": [ + "ga": [ "granted_audience-0009_1" ], - "ConsentRemember": true, - "ConsentRememberFor": 9, - "ConsentHandledAt": null, - "ConsentWasHandled": true, - "ConsentError": { + "ce": true, + "cf": 9, + "ch": null, + "cw": true, + "cx": { "error": "", "error_description": "", "error_hint": "", @@ -63,10 +58,10 @@ "error_debug": "", "valid": false }, - "SessionIDToken": { + "st": { "session_id_token-0009": "0009" }, - "SessionAccessToken": { + "sa": { "session_access_token-0009": "0009" } } diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0010.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0010.json index 6b8f3cf7a10..353ed37ffe5 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0010.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0010.json @@ -1,38 +1,33 @@ { - "ID": "challenge-0010", - "NID": "00000000-0000-0000-0000-000000000000", - "RequestedScope": [ + "i": "challenge-0010", + "n": "00000000-0000-0000-0000-000000000000", + "rs": [ "requested_scope-0010_1" ], - "RequestedAudience": [ + "ra": [ "requested_audience-0010_1" ], - "LoginSkip": true, - "Subject": "subject-0010", - "OpenIDConnectContext": { + "ls": true, + "s": "subject-0010", + "oc": { "display": "display-0010" }, - "Client": null, - "ClientID": "", - "RequestURL": "http://request/0010", - "SessionID": "auth_session-0010", - "IdentityProviderSessionID": "", - "LoginVerifier": "verifier-0010", - "LoginCSRF": "csrf-0010", - "LoginInitializedAt": null, - "RequestedAt": "0001-01-01T00:00:00Z", - "State": 128, - "LoginRemember": true, - "LoginRememberFor": 10, - "LoginExtendSessionLifespan": false, - "ACR": "acr-0010", - "AMR": [], - "ForceSubjectIdentifier": "force_subject_id-0010", - "Context": { + "r": "http://request/0010", + "si": "auth_session-0010", + "lv": "verifier-0010", + "lc": "csrf-0010", + "li": null, + "ia": "0001-01-01T00:00:00Z", + "q": 128, + "lr": true, + "lf": 10, + "a": "acr-0010", + "fs": "force_subject_id-0010", + "ct": { "context": "0010" }, - "LoginWasUsed": true, - "LoginError": { + "lu": true, + "le": { "error": "", "error_description": "", "error_hint": "", @@ -40,22 +35,22 @@ "error_debug": "", "valid": false }, - "LoginAuthenticatedAt": null, - "ConsentChallengeID": "challenge-0010", - "ConsentSkip": true, - "ConsentVerifier": "verifier-0010", - "ConsentCSRF": "csrf-0010", - "GrantedScope": [ + "la": null, + "cc": "challenge-0010", + "cs": true, + "cv": "verifier-0010", + "cr": "csrf-0010", + "gs": [ "granted_scope-0010_1" ], - "GrantedAudience": [ + "ga": [ "granted_audience-0010_1" ], - "ConsentRemember": true, - "ConsentRememberFor": 10, - "ConsentHandledAt": null, - "ConsentWasHandled": true, - "ConsentError": { + "ce": true, + "cf": 10, + "ch": null, + "cw": true, + "cx": { "error": "", "error_description": "", "error_hint": "", @@ -63,10 +58,10 @@ "error_debug": "", "valid": false }, - "SessionIDToken": { + "st": { "session_id_token-0010": "0010" }, - "SessionAccessToken": { + "sa": { "session_access_token-0010": "0010" } } diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0011.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0011.json index 5b11c1941d1..ed92bbce294 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0011.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0011.json @@ -1,38 +1,33 @@ { - "ID": "challenge-0011", - "NID": "00000000-0000-0000-0000-000000000000", - "RequestedScope": [ + "i": "challenge-0011", + "n": "00000000-0000-0000-0000-000000000000", + "rs": [ "requested_scope-0011_1" ], - "RequestedAudience": [ + "ra": [ "requested_audience-0011_1" ], - "LoginSkip": true, - "Subject": "subject-0011", - "OpenIDConnectContext": { + "ls": true, + "s": "subject-0011", + "oc": { "display": "display-0011" }, - "Client": null, - "ClientID": "", - "RequestURL": "http://request/0011", - "SessionID": "auth_session-0011", - "IdentityProviderSessionID": "", - "LoginVerifier": "verifier-0011", - "LoginCSRF": "csrf-0011", - "LoginInitializedAt": null, - "RequestedAt": "0001-01-01T00:00:00Z", - "State": 128, - "LoginRemember": true, - "LoginRememberFor": 11, - "LoginExtendSessionLifespan": false, - "ACR": "acr-0011", - "AMR": [], - "ForceSubjectIdentifier": "force_subject_id-0011", - "Context": { + "r": "http://request/0011", + "si": "auth_session-0011", + "lv": "verifier-0011", + "lc": "csrf-0011", + "li": null, + "ia": "0001-01-01T00:00:00Z", + "q": 128, + "lr": true, + "lf": 11, + "a": "acr-0011", + "fs": "force_subject_id-0011", + "ct": { "context": "0011" }, - "LoginWasUsed": true, - "LoginError": { + "lu": true, + "le": { "error": "", "error_description": "", "error_hint": "", @@ -40,22 +35,22 @@ "error_debug": "", "valid": false }, - "LoginAuthenticatedAt": null, - "ConsentChallengeID": "challenge-0011", - "ConsentSkip": true, - "ConsentVerifier": "verifier-0011", - "ConsentCSRF": "csrf-0011", - "GrantedScope": [ + "la": null, + "cc": "challenge-0011", + "cs": true, + "cv": "verifier-0011", + "cr": "csrf-0011", + "gs": [ "granted_scope-0011_1" ], - "GrantedAudience": [ + "ga": [ "granted_audience-0011_1" ], - "ConsentRemember": true, - "ConsentRememberFor": 11, - "ConsentHandledAt": null, - "ConsentWasHandled": true, - "ConsentError": { + "ce": true, + "cf": 11, + "ch": null, + "cw": true, + "cx": { "error": "", "error_description": "", "error_hint": "", @@ -63,10 +58,10 @@ "error_debug": "", "valid": false }, - "SessionIDToken": { + "st": { "session_id_token-0011": "0011" }, - "SessionAccessToken": { + "sa": { "session_access_token-0011": "0011" } } diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0012.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0012.json index b648871cda5..6375e369280 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0012.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0012.json @@ -1,38 +1,33 @@ { - "ID": "challenge-0012", - "NID": "00000000-0000-0000-0000-000000000000", - "RequestedScope": [ + "i": "challenge-0012", + "n": "00000000-0000-0000-0000-000000000000", + "rs": [ "requested_scope-0012_1" ], - "RequestedAudience": [ + "ra": [ "requested_audience-0012_1" ], - "LoginSkip": true, - "Subject": "subject-0012", - "OpenIDConnectContext": { + "ls": true, + "s": "subject-0012", + "oc": { "display": "display-0012" }, - "Client": null, - "ClientID": "", - "RequestURL": "http://request/0012", - "SessionID": "auth_session-0012", - "IdentityProviderSessionID": "", - "LoginVerifier": "verifier-0012", - "LoginCSRF": "csrf-0012", - "LoginInitializedAt": null, - "RequestedAt": "0001-01-01T00:00:00Z", - "State": 128, - "LoginRemember": true, - "LoginRememberFor": 12, - "LoginExtendSessionLifespan": false, - "ACR": "acr-0012", - "AMR": [], - "ForceSubjectIdentifier": "force_subject_id-0012", - "Context": { + "r": "http://request/0012", + "si": "auth_session-0012", + "lv": "verifier-0012", + "lc": "csrf-0012", + "li": null, + "ia": "0001-01-01T00:00:00Z", + "q": 128, + "lr": true, + "lf": 12, + "a": "acr-0012", + "fs": "force_subject_id-0012", + "ct": { "context": "0012" }, - "LoginWasUsed": true, - "LoginError": { + "lu": true, + "le": { "error": "", "error_description": "", "error_hint": "", @@ -40,22 +35,22 @@ "error_debug": "", "valid": false }, - "LoginAuthenticatedAt": null, - "ConsentChallengeID": "challenge-0012", - "ConsentSkip": true, - "ConsentVerifier": "verifier-0012", - "ConsentCSRF": "csrf-0012", - "GrantedScope": [ + "la": null, + "cc": "challenge-0012", + "cs": true, + "cv": "verifier-0012", + "cr": "csrf-0012", + "gs": [ "granted_scope-0012_1" ], - "GrantedAudience": [ + "ga": [ "granted_audience-0012_1" ], - "ConsentRemember": true, - "ConsentRememberFor": 12, - "ConsentHandledAt": null, - "ConsentWasHandled": true, - "ConsentError": { + "ce": true, + "cf": 12, + "ch": null, + "cw": true, + "cx": { "error": "", "error_description": "", "error_hint": "", @@ -63,10 +58,10 @@ "error_debug": "", "valid": false }, - "SessionIDToken": { + "st": { "session_id_token-0012": "0012" }, - "SessionAccessToken": { + "sa": { "session_access_token-0012": "0012" } } diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0013.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0013.json index 1f28bb67647..3939f00e959 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0013.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0013.json @@ -1,38 +1,33 @@ { - "ID": "challenge-0013", - "NID": "00000000-0000-0000-0000-000000000000", - "RequestedScope": [ + "i": "challenge-0013", + "n": "00000000-0000-0000-0000-000000000000", + "rs": [ "requested_scope-0013_1" ], - "RequestedAudience": [ + "ra": [ "requested_audience-0013_1" ], - "LoginSkip": true, - "Subject": "subject-0013", - "OpenIDConnectContext": { + "ls": true, + "s": "subject-0013", + "oc": { "display": "display-0013" }, - "Client": null, - "ClientID": "", - "RequestURL": "http://request/0013", - "SessionID": "auth_session-0013", - "IdentityProviderSessionID": "", - "LoginVerifier": "verifier-0013", - "LoginCSRF": "csrf-0013", - "LoginInitializedAt": null, - "RequestedAt": "0001-01-01T00:00:00Z", - "State": 128, - "LoginRemember": true, - "LoginRememberFor": 13, - "LoginExtendSessionLifespan": false, - "ACR": "acr-0013", - "AMR": [], - "ForceSubjectIdentifier": "force_subject_id-0013", - "Context": { + "r": "http://request/0013", + "si": "auth_session-0013", + "lv": "verifier-0013", + "lc": "csrf-0013", + "li": null, + "ia": "0001-01-01T00:00:00Z", + "q": 128, + "lr": true, + "lf": 13, + "a": "acr-0013", + "fs": "force_subject_id-0013", + "ct": { "context": "0013" }, - "LoginWasUsed": true, - "LoginError": { + "lu": true, + "le": { "error": "", "error_description": "", "error_hint": "", @@ -40,22 +35,22 @@ "error_debug": "", "valid": false }, - "LoginAuthenticatedAt": null, - "ConsentChallengeID": "challenge-0013", - "ConsentSkip": true, - "ConsentVerifier": "verifier-0013", - "ConsentCSRF": "csrf-0013", - "GrantedScope": [ + "la": null, + "cc": "challenge-0013", + "cs": true, + "cv": "verifier-0013", + "cr": "csrf-0013", + "gs": [ "granted_scope-0013_1" ], - "GrantedAudience": [ + "ga": [ "granted_audience-0013_1" ], - "ConsentRemember": true, - "ConsentRememberFor": 13, - "ConsentHandledAt": null, - "ConsentWasHandled": true, - "ConsentError": { + "ce": true, + "cf": 13, + "ch": null, + "cw": true, + "cx": { "error": "", "error_description": "", "error_hint": "", @@ -63,10 +58,10 @@ "error_debug": "", "valid": false }, - "SessionIDToken": { + "st": { "session_id_token-0013": "0013" }, - "SessionAccessToken": { + "sa": { "session_access_token-0013": "0013" } } diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0014.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0014.json index 3e2dd7bb444..38e0af54056 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0014.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0014.json @@ -1,38 +1,33 @@ { - "ID": "challenge-0014", - "NID": "00000000-0000-0000-0000-000000000000", - "RequestedScope": [ + "i": "challenge-0014", + "n": "00000000-0000-0000-0000-000000000000", + "rs": [ "requested_scope-0014_1" ], - "RequestedAudience": [ + "ra": [ "requested_audience-0014_1" ], - "LoginSkip": true, - "Subject": "subject-0014", - "OpenIDConnectContext": { + "ls": true, + "s": "subject-0014", + "oc": { "display": "display-0014" }, - "Client": null, - "ClientID": "", - "RequestURL": "http://request/0014", - "SessionID": "auth_session-0014", - "IdentityProviderSessionID": "", - "LoginVerifier": "verifier-0014", - "LoginCSRF": "csrf-0014", - "LoginInitializedAt": null, - "RequestedAt": "0001-01-01T00:00:00Z", - "State": 128, - "LoginRemember": true, - "LoginRememberFor": 14, - "LoginExtendSessionLifespan": false, - "ACR": "acr-0014", - "AMR": [], - "ForceSubjectIdentifier": "force_subject_id-0014", - "Context": { + "r": "http://request/0014", + "si": "auth_session-0014", + "lv": "verifier-0014", + "lc": "csrf-0014", + "li": null, + "ia": "0001-01-01T00:00:00Z", + "q": 128, + "lr": true, + "lf": 14, + "a": "acr-0014", + "fs": "force_subject_id-0014", + "ct": { "context": "0014" }, - "LoginWasUsed": true, - "LoginError": { + "lu": true, + "le": { "error": "", "error_description": "", "error_hint": "", @@ -40,22 +35,22 @@ "error_debug": "", "valid": false }, - "LoginAuthenticatedAt": null, - "ConsentChallengeID": "challenge-0014", - "ConsentSkip": true, - "ConsentVerifier": "verifier-0014", - "ConsentCSRF": "csrf-0014", - "GrantedScope": [ + "la": null, + "cc": "challenge-0014", + "cs": true, + "cv": "verifier-0014", + "cr": "csrf-0014", + "gs": [ "granted_scope-0014_1" ], - "GrantedAudience": [ + "ga": [ "granted_audience-0014_1" ], - "ConsentRemember": true, - "ConsentRememberFor": 14, - "ConsentHandledAt": null, - "ConsentWasHandled": true, - "ConsentError": { + "ce": true, + "cf": 14, + "ch": null, + "cw": true, + "cx": { "error": "", "error_description": "", "error_hint": "", @@ -63,10 +58,10 @@ "error_debug": "", "valid": false }, - "SessionIDToken": { + "st": { "session_id_token-0014": "0014" }, - "SessionAccessToken": { + "sa": { "session_access_token-0014": "0014" } } diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0015.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0015.json index 5723154839a..f55d9d59c0a 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0015.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0015.json @@ -1,43 +1,39 @@ { - "ID": "challenge-0015", - "NID": "00000000-0000-0000-0000-000000000000", - "RequestedScope": [ + "i": "challenge-0015", + "n": "00000000-0000-0000-0000-000000000000", + "rs": [ "requested_scope-0015_1", "requested_scope-0015_2" ], - "RequestedAudience": [ + "ra": [ "requested_audience-0015_1", "requested_audience-0015_2" ], - "LoginSkip": true, - "Subject": "subject-0015", - "OpenIDConnectContext": { + "ls": true, + "s": "subject-0015", + "oc": { "display": "display-0015" }, - "Client": null, - "ClientID": "", - "RequestURL": "http://request/0015", - "SessionID": "auth_session-0015", - "IdentityProviderSessionID": "", - "LoginVerifier": "verifier-0015", - "LoginCSRF": "csrf-0015", - "LoginInitializedAt": null, - "RequestedAt": "0001-01-01T00:00:00Z", - "State": 128, - "LoginRemember": true, - "LoginRememberFor": 15, - "LoginExtendSessionLifespan": false, - "ACR": "acr-0015", - "AMR": [ + "r": "http://request/0015", + "si": "auth_session-0015", + "lv": "verifier-0015", + "lc": "csrf-0015", + "li": null, + "ia": "0001-01-01T00:00:00Z", + "q": 128, + "lr": true, + "lf": 15, + "a": "acr-0015", + "am": [ "amr-0015-1", "amr-0015-2" ], - "ForceSubjectIdentifier": "force_subject_id-0015", - "Context": { + "fs": "force_subject_id-0015", + "ct": { "context": "0015" }, - "LoginWasUsed": true, - "LoginError": { + "lu": true, + "le": { "error": "", "error_description": "", "error_hint": "", @@ -45,24 +41,24 @@ "error_debug": "", "valid": false }, - "LoginAuthenticatedAt": null, - "ConsentChallengeID": "challenge-0015", - "ConsentSkip": true, - "ConsentVerifier": "verifier-0015", - "ConsentCSRF": "csrf-0015", - "GrantedScope": [ + "la": null, + "cc": "challenge-0015", + "cs": true, + "cv": "verifier-0015", + "cr": "csrf-0015", + "gs": [ "granted_scope-0015_1", "granted_scope-0015_2" ], - "GrantedAudience": [ + "ga": [ "granted_audience-0015_1", "granted_audience-0015_2" ], - "ConsentRemember": true, - "ConsentRememberFor": 15, - "ConsentHandledAt": null, - "ConsentWasHandled": true, - "ConsentError": { + "ce": true, + "cf": 15, + "ch": null, + "cw": true, + "cx": { "error": "", "error_description": "", "error_hint": "", @@ -70,10 +66,10 @@ "error_debug": "", "valid": false }, - "SessionIDToken": { + "st": { "session_id_token-0015": "0015" }, - "SessionAccessToken": { + "sa": { "session_access_token-0015": "0015" } } diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0016.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0016.json index e653059fe46..be6ca67a2d1 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0016.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0016.json @@ -1,43 +1,40 @@ { - "ID": "challenge-0016", - "NID": "00000000-0000-0000-0000-000000000000", - "RequestedScope": [ + "i": "challenge-0016", + "n": "00000000-0000-0000-0000-000000000000", + "rs": [ "requested_scope-0016_1", "requested_scope-0016_2" ], - "RequestedAudience": [ + "ra": [ "requested_audience-0016_1", "requested_audience-0016_2" ], - "LoginSkip": true, - "Subject": "subject-0016", - "OpenIDConnectContext": { + "ls": true, + "s": "subject-0016", + "oc": { "display": "display-0016" }, - "Client": null, - "ClientID": "", - "RequestURL": "http://request/0016", - "SessionID": "auth_session-0016", - "IdentityProviderSessionID": "", - "LoginVerifier": "verifier-0016", - "LoginCSRF": "csrf-0016", - "LoginInitializedAt": null, - "RequestedAt": "0001-01-01T00:00:00Z", - "State": 128, - "LoginRemember": true, - "LoginRememberFor": 15, - "LoginExtendSessionLifespan": true, - "ACR": "acr-0016", - "AMR": [ + "r": "http://request/0016", + "si": "auth_session-0016", + "lv": "verifier-0016", + "lc": "csrf-0016", + "li": null, + "ia": "0001-01-01T00:00:00Z", + "q": 128, + "lr": true, + "lf": 15, + "ll": true, + "a": "acr-0016", + "am": [ "amr-0016-1", "amr-0016-2" ], - "ForceSubjectIdentifier": "force_subject_id-0016", - "Context": { + "fs": "force_subject_id-0016", + "ct": { "context": "0016" }, - "LoginWasUsed": true, - "LoginError": { + "lu": true, + "le": { "error": "", "error_description": "", "error_hint": "", @@ -45,24 +42,24 @@ "error_debug": "", "valid": false }, - "LoginAuthenticatedAt": null, - "ConsentChallengeID": "challenge-0016", - "ConsentSkip": true, - "ConsentVerifier": "verifier-0016", - "ConsentCSRF": "csrf-0016", - "GrantedScope": [ + "la": null, + "cc": "challenge-0016", + "cs": true, + "cv": "verifier-0016", + "cr": "csrf-0016", + "gs": [ "granted_scope-0016_1", "granted_scope-0016_2" ], - "GrantedAudience": [ + "ga": [ "granted_audience-0016_1", "granted_audience-0016_2" ], - "ConsentRemember": true, - "ConsentRememberFor": 15, - "ConsentHandledAt": null, - "ConsentWasHandled": true, - "ConsentError": { + "ce": true, + "cf": 15, + "ch": null, + "cw": true, + "cx": { "error": "", "error_description": "", "error_hint": "", @@ -70,10 +67,10 @@ "error_debug": "", "valid": false }, - "SessionIDToken": { + "st": { "session_id_token-0016": "0016" }, - "SessionAccessToken": { + "sa": { "session_access_token-0016": "0016" } } diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0017.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0017.json index fd65dab7164..e8f9235696b 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0017.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0017.json @@ -1,43 +1,41 @@ { - "ID": "challenge-0017", - "NID": "00000000-0000-0000-0000-000000000000", - "RequestedScope": [ + "i": "challenge-0017", + "n": "00000000-0000-0000-0000-000000000000", + "rs": [ "requested_scope-0016_1", "requested_scope-0016_2" ], - "RequestedAudience": [ + "ra": [ "requested_audience-0016_1", "requested_audience-0016_2" ], - "LoginSkip": true, - "Subject": "subject-0017", - "OpenIDConnectContext": { + "ls": true, + "s": "subject-0017", + "oc": { "display": "display-0017" }, - "Client": null, - "ClientID": "", - "RequestURL": "http://request/0017", - "SessionID": "auth_session-0017", - "IdentityProviderSessionID": "identity_provider_session_id-0017", - "LoginVerifier": "verifier-0017", - "LoginCSRF": "csrf-0017", - "LoginInitializedAt": null, - "RequestedAt": "0001-01-01T00:00:00Z", - "State": 128, - "LoginRemember": true, - "LoginRememberFor": 15, - "LoginExtendSessionLifespan": true, - "ACR": "acr-0017", - "AMR": [ + "r": "http://request/0017", + "si": "auth_session-0017", + "is": "identity_provider_session_id-0017", + "lv": "verifier-0017", + "lc": "csrf-0017", + "li": null, + "ia": "0001-01-01T00:00:00Z", + "q": 128, + "lr": true, + "lf": 15, + "ll": true, + "a": "acr-0017", + "am": [ "amr-0017-1", "amr-0017-2" ], - "ForceSubjectIdentifier": "force_subject_id-0017", - "Context": { + "fs": "force_subject_id-0017", + "ct": { "context": "0017" }, - "LoginWasUsed": true, - "LoginError": { + "lu": true, + "le": { "error": "", "error_description": "", "error_hint": "", @@ -45,24 +43,24 @@ "error_debug": "", "valid": false }, - "LoginAuthenticatedAt": null, - "ConsentChallengeID": "challenge-0017", - "ConsentSkip": true, - "ConsentVerifier": "verifier-0017", - "ConsentCSRF": "csrf-0017", - "GrantedScope": [ + "la": null, + "cc": "challenge-0017", + "cs": true, + "cv": "verifier-0017", + "cr": "csrf-0017", + "gs": [ "granted_scope-0016_1", "granted_scope-0016_2" ], - "GrantedAudience": [ + "ga": [ "granted_audience-0016_1", "granted_audience-0016_2" ], - "ConsentRemember": true, - "ConsentRememberFor": 15, - "ConsentHandledAt": null, - "ConsentWasHandled": true, - "ConsentError": { + "ce": true, + "cf": 15, + "ch": null, + "cw": true, + "cx": { "error": "", "error_description": "", "error_hint": "", @@ -70,10 +68,10 @@ "error_debug": "", "valid": false }, - "SessionIDToken": { + "st": { "session_id_token-0017": "0017" }, - "SessionAccessToken": { + "sa": { "session_access_token-0017": "0017" } } diff --git a/persistence/sql/migratest/migration_test.go b/persistence/sql/migratest/migration_test.go index 1fa0ce3836d..8564cfab969 100644 --- a/persistence/sql/migratest/migration_test.go +++ b/persistence/sql/migratest/migration_test.go @@ -144,7 +144,7 @@ func TestMigrations(t *testing.T) { t.Run("case=hydra_oauth2_authentication_session", func(t *testing.T) { ss := []flow.LoginSession{} - c.All(&ss) + require.NoError(t, c.All(&ss)) require.Equal(t, 17, len(ss)) for _, s := range ss { @@ -157,7 +157,7 @@ func TestMigrations(t *testing.T) { t.Run("case=hydra_oauth2_obfuscated_authentication_session", func(t *testing.T) { ss := []consent.ForcedObfuscatedLoginSession{} - c.All(&ss) + require.NoError(t, c.All(&ss)) require.Equal(t, 13, len(ss)) for _, s := range ss { @@ -169,7 +169,7 @@ func TestMigrations(t *testing.T) { t.Run("case=hydra_oauth2_logout_request", func(t *testing.T) { lrs := []flow.LogoutRequest{} - c.All(&lrs) + require.NoError(t, c.All(&lrs)) require.Equal(t, 7, len(lrs)) for _, s := range lrs { @@ -182,7 +182,7 @@ func TestMigrations(t *testing.T) { t.Run("case=hydra_oauth2_jti_blacklist", func(t *testing.T) { bjtis := []oauth2.BlacklistedJTI{} - c.All(&bjtis) + require.NoError(t, c.All(&bjtis)) require.Equal(t, 1, len(bjtis)) for _, bjti := range bjtis { testhelpersuuid.AssertUUID(t, bjti.NID) @@ -194,7 +194,7 @@ func TestMigrations(t *testing.T) { t.Run("case=hydra_oauth2_access", func(t *testing.T) { as := []sql.OAuth2RequestSQL{} - c.RawQuery("SELECT * FROM hydra_oauth2_access").All(&as) + require.NoError(t, c.RawQuery("SELECT * FROM hydra_oauth2_access").All(&as)) require.Equal(t, 13, len(as)) for _, a := range as { @@ -210,7 +210,7 @@ func TestMigrations(t *testing.T) { t.Run("case=hydra_oauth2_refresh", func(t *testing.T) { rs := []sql.OAuth2RequestSQL{} - c.RawQuery("SELECT * FROM hydra_oauth2_refresh").All(&rs) + require.NoError(t, c.RawQuery(`SELECT signature, nid, request_id, challenge_id, requested_at, client_id, scope, granted_scope, requested_audience, granted_audience, form_data, subject, active, session_data, expires_at FROM hydra_oauth2_refresh`).All(&rs)) require.Equal(t, 13, len(rs)) for _, r := range rs { @@ -226,7 +226,7 @@ func TestMigrations(t *testing.T) { t.Run("case=hydra_oauth2_code", func(t *testing.T) { cs := []sql.OAuth2RequestSQL{} - c.RawQuery("SELECT * FROM hydra_oauth2_code").All(&cs) + require.NoError(t, c.RawQuery("SELECT * FROM hydra_oauth2_code").All(&cs)) require.Equal(t, 13, len(cs)) for _, c := range cs { @@ -242,7 +242,7 @@ func TestMigrations(t *testing.T) { t.Run("case=hydra_oauth2_oidc", func(t *testing.T) { os := []sql.OAuth2RequestSQL{} - c.RawQuery("SELECT * FROM hydra_oauth2_oidc").All(&os) + require.NoError(t, c.RawQuery("SELECT * FROM hydra_oauth2_oidc").All(&os)) require.Equal(t, 13, len(os)) for _, o := range os { @@ -258,7 +258,7 @@ func TestMigrations(t *testing.T) { t.Run("case=hydra_oauth2_pkce", func(t *testing.T) { ps := []sql.OAuth2RequestSQL{} - c.RawQuery("SELECT * FROM hydra_oauth2_pkce").All(&ps) + require.NoError(t, c.RawQuery("SELECT * FROM hydra_oauth2_pkce").All(&ps)) require.Equal(t, 11, len(ps)) for _, p := range ps { @@ -274,7 +274,7 @@ func TestMigrations(t *testing.T) { t.Run("case=networks", func(t *testing.T) { ns := []networkx.Network{} - c.RawQuery("SELECT * FROM networks").All(&ns) + require.NoError(t, c.RawQuery("SELECT * FROM networks").All(&ns)) require.Equal(t, 1, len(ns)) for _, n := range ns { testhelpersuuid.AssertUUID(t, n.ID) diff --git a/persistence/sql/migrations/20241012144910000001_unused_indices.down.sql b/persistence/sql/migrations/20241012144910000001_unused_indices.down.sql new file mode 100644 index 00000000000..ea765c5859b --- /dev/null +++ b/persistence/sql/migrations/20241012144910000001_unused_indices.down.sql @@ -0,0 +1,14 @@ +-- CREATE INDEX IF NOT EXISTS hydra_oauth2_access_client_id_subject_idx ON hydra_oauth2_access (client_id ASC, subject ASC, nid ASC); +CREATE INDEX IF NOT EXISTS hydra_oauth2_access_expires_at_v2_idx ON hydra_oauth2_access (expires_at ASC); + +CREATE INDEX IF NOT EXISTS hydra_oauth2_refresh_client_id_subject_idx ON hydra_oauth2_refresh (client_id ASC, subject ASC); +CREATE INDEX IF NOT EXISTS hydra_oauth2_refresh_expires_at_v2_idx ON hydra_oauth2_refresh (expires_at ASC); + +CREATE INDEX IF NOT EXISTS hydra_oauth2_pkce_request_id_idx ON hydra_oauth2_pkce (request_id ASC, nid ASC); +CREATE INDEX IF NOT EXISTS hydra_oauth2_pkce_expires_at_v2_idx ON hydra_oauth2_pkce (expires_at ASC); + +CREATE INDEX IF NOT EXISTS hydra_oauth2_oidc_request_id_idx ON hydra_oauth2_oidc (request_id ASC, nid ASC); +CREATE INDEX IF NOT EXISTS hydra_oauth2_oidc_expires_at_idx ON hydra_oauth2_oidc (expires_at ASC); + +CREATE INDEX IF NOT EXISTS hydra_oauth2_pkce_request_id_idx ON hydra_oauth2_code (request_id ASC, nid ASC); +CREATE INDEX IF NOT EXISTS hydra_oauth2_pkce_expires_at_v2_idx ON hydra_oauth2_code (expires_at ASC); diff --git a/persistence/sql/migrations/20241012144910000001_unused_indices.mysql.up.sql b/persistence/sql/migrations/20241012144910000001_unused_indices.mysql.up.sql new file mode 100644 index 00000000000..83ae09f7edf --- /dev/null +++ b/persistence/sql/migrations/20241012144910000001_unused_indices.mysql.up.sql @@ -0,0 +1,14 @@ +-- DROP INDEX hydra_oauth2_access_client_id_subject_idx ON hydra_oauth2_access; +DROP INDEX hydra_oauth2_access_expires_at_v2_idx ON hydra_oauth2_access; -- janitor still uses requested_at index + +DROP INDEX hydra_oauth2_refresh_client_id_subject_idx ON hydra_oauth2_refresh; +DROP INDEX hydra_oauth2_refresh_expires_at_v2_idx ON hydra_oauth2_refresh; -- janitor still uses requested_at index + +DROP INDEX hydra_oauth2_pkce_request_id_idx ON hydra_oauth2_pkce; +DROP INDEX hydra_oauth2_pkce_expires_at_v2_idx ON hydra_oauth2_pkce; -- janitor still uses requested_at index + +DROP INDEX hydra_oauth2_oidc_request_id_idx ON hydra_oauth2_oidc; +DROP INDEX hydra_oauth2_oidc_expires_at_idx ON hydra_oauth2_oidc; -- janitor still uses requested_at index + +DROP INDEX hydra_oauth2_code_request_id_idx ON hydra_oauth2_code; +DROP INDEX hydra_oauth2_code_expires_at_v2_idx ON hydra_oauth2_code; -- janitor still uses requested_at index diff --git a/persistence/sql/migrations/20241012144910000001_unused_indices.up.sql b/persistence/sql/migrations/20241012144910000001_unused_indices.up.sql new file mode 100644 index 00000000000..ac2ba6ebf8b --- /dev/null +++ b/persistence/sql/migrations/20241012144910000001_unused_indices.up.sql @@ -0,0 +1,14 @@ +DROP INDEX IF EXISTS hydra_oauth2_access_client_id_subject_idx; +DROP INDEX IF EXISTS hydra_oauth2_access_expires_at_v2_idx; -- janitor still uses requested_at index + +DROP INDEX IF EXISTS hydra_oauth2_refresh_client_id_subject_idx; +DROP INDEX IF EXISTS hydra_oauth2_refresh_expires_at_v2_idx; -- janitor still uses requested_at index + +DROP INDEX IF EXISTS hydra_oauth2_pkce_request_id_idx; +DROP INDEX IF EXISTS hydra_oauth2_pkce_expires_at_v2_idx; -- janitor still uses requested_at index + +DROP INDEX IF EXISTS hydra_oauth2_oidc_request_id_idx; +DROP INDEX IF EXISTS hydra_oauth2_oidc_expires_at_idx; -- janitor still uses requested_at index + +DROP INDEX IF EXISTS hydra_oauth2_code_request_id_idx; +DROP INDEX IF EXISTS hydra_oauth2_code_expires_at_v2_idx; -- janitor still uses requested_at index diff --git a/persistence/sql/migrations/20241014121000000000_add_refresh_token_in_grace_period_flag.down.sql b/persistence/sql/migrations/20241014121000000000_add_refresh_token_in_grace_period_flag.down.sql new file mode 100644 index 00000000000..a30a127e902 --- /dev/null +++ b/persistence/sql/migrations/20241014121000000000_add_refresh_token_in_grace_period_flag.down.sql @@ -0,0 +1 @@ +ALTER TABLE hydra_oauth2_refresh DROP COLUMN first_used_at; diff --git a/persistence/sql/migrations/20241014121000000000_add_refresh_token_in_grace_period_flag.up.sql b/persistence/sql/migrations/20241014121000000000_add_refresh_token_in_grace_period_flag.up.sql new file mode 100644 index 00000000000..8ae823047f7 --- /dev/null +++ b/persistence/sql/migrations/20241014121000000000_add_refresh_token_in_grace_period_flag.up.sql @@ -0,0 +1 @@ +ALTER TABLE hydra_oauth2_refresh ADD first_used_at TIMESTAMP DEFAULT NULL; diff --git a/persistence/sql/persister.go b/persistence/sql/persister.go index 93649fc46ef..ba2647393a5 100644 --- a/persistence/sql/persister.go +++ b/persistence/sql/persister.go @@ -60,6 +60,7 @@ type ( contextx.Provider x.RegistryLogger x.TracingProvider + config.Provider } ) @@ -177,9 +178,12 @@ func (p *Persister) Connection(ctx context.Context) *pop.Connection { return popx.GetConnection(ctx, p.conn) } -func (p *Persister) Ping() error { - type pinger interface{ Ping() error } - return p.conn.Store.(pinger).Ping() +func (p *Persister) Ping(ctx context.Context) error { + return p.conn.Store.SQLDB().PingContext(ctx) +} +func (p *Persister) PingContext(ctx context.Context) error { + type pinger interface{ PingContext(context.Context) error } + return p.conn.Store.(pinger).PingContext(ctx) } func (p *Persister) mustSetNetwork(nid uuid.UUID, v interface{}) interface{} { diff --git a/persistence/sql/persister_authenticate.go b/persistence/sql/persister_authenticate.go index 4fdc7eff0ae..013ccc30051 100644 --- a/persistence/sql/persister_authenticate.go +++ b/persistence/sql/persister_authenticate.go @@ -3,8 +3,14 @@ package sql -import "context" +import ( + "context" +) -func (p *Persister) Authenticate(ctx context.Context, name, secret string) error { - return p.r.Kratos().Authenticate(ctx, name, secret) +func (p *Persister) Authenticate(ctx context.Context, name, secret string) (subject string, err error) { + session, err := p.r.Kratos().Authenticate(ctx, name, secret) + if err != nil { + return "", err + } + return session.Identity.Id, nil } diff --git a/persistence/sql/persister_consent.go b/persistence/sql/persister_consent.go index 5bd97139380..ad3c25b3d72 100644 --- a/persistence/sql/persister_consent.go +++ b/persistence/sql/persister_consent.go @@ -388,8 +388,7 @@ func (p *Persister) ConfirmLoginSession(ctx context.Context, loginSession *flow. return p.mySQLConfirmLoginSession(ctx, loginSession) } - err = p.Connection(ctx).Transaction(func(tx *pop.Connection) error { - res, err := tx.TX.NamedExec(` + res, err := p.Connection(ctx).Store.NamedExecContext(ctx, ` INSERT INTO hydra_oauth2_authentication_session (id, nid, authenticated_at, subject, remember, identity_provider_session_id) VALUES (:id, :nid, :authenticated_at, :subject, :remember, :identity_provider_session_id) ON CONFLICT(id) DO @@ -400,22 +399,16 @@ UPDATE SET identity_provider_session_id = :identity_provider_session_id WHERE hydra_oauth2_authentication_session.id = :id AND hydra_oauth2_authentication_session.nid = :nid `, loginSession) - if err != nil { - return sqlcon.HandleError(err) - } - n, err := res.RowsAffected() - if err != nil { - return sqlcon.HandleError(err) - } - if n == 0 { - return errorsx.WithStack(x.ErrNotFound) - } - return nil - }) if err != nil { - return errors.WithStack(err) + return sqlcon.HandleError(err) + } + n, err := res.RowsAffected() + if err != nil { + return sqlcon.HandleError(err) + } + if n == 0 { + return errorsx.WithStack(x.ErrNotFound) } - return nil } diff --git a/persistence/sql/persister_oauth2.go b/persistence/sql/persister_oauth2.go index 8334a072294..2585a3a159d 100644 --- a/persistence/sql/persister_oauth2.go +++ b/persistence/sql/persister_oauth2.go @@ -58,6 +58,10 @@ type ( // InternalExpiresAt denormalizes the expiry from the session to additionally store it as a row. InternalExpiresAt sqlxx.NullTime `db:"expires_at" json:"-"` } + OAuth2RefreshTable struct { + OAuth2RequestSQL + FirstUsedAt sql.NullTime `db:"first_used_at"` + } ) const ( @@ -72,6 +76,10 @@ func (r OAuth2RequestSQL) TableName() string { return "hydra_oauth2_" + string(r.Table) } +func (r OAuth2RefreshTable) TableName() string { + return "hydra_oauth2_refresh" +} + func (p *Persister) sqlSchemaFromRequest(ctx context.Context, signature string, r fosite.Requester, table tableName, expiresAt time.Time) (*OAuth2RequestSQL, error) { subject := "" if r.GetSession() == nil { @@ -122,6 +130,24 @@ func (p *Persister) sqlSchemaFromRequest(ctx context.Context, signature string, }, nil } +func (p *Persister) marshalSession(ctx context.Context, session fosite.Session) ([]byte, error) { + sessionBytes, err := json.Marshal(session) + if err != nil { + return nil, err + } + + if !p.config.EncryptSessionData(ctx) { + return sessionBytes, nil + } + + ciphertext, err := p.r.KeyCipher().Encrypt(ctx, sessionBytes, nil) + if err != nil { + return nil, err + } + + return []byte(ciphertext), nil +} + func (r *OAuth2RequestSQL) toRequest(ctx context.Context, session fosite.Session, p *Persister) (_ *fosite.Request, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.toRequest") defer otelx.End(span, &err) @@ -439,7 +465,34 @@ func (p *Persister) CreateRefreshTokenSession(ctx context.Context, signature str func (p *Persister) GetRefreshTokenSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetRefreshTokenSession") defer otelx.End(span, &err) - return p.findSessionBySignature(ctx, signature, session, sqlTableRefresh) + + r := OAuth2RefreshTable{OAuth2RequestSQL: OAuth2RequestSQL{Table: sqlTableRefresh}} + err = p.QueryWithNetwork(ctx).Where("signature = ?", signature).First(&r) + if errors.Is(err, sql.ErrNoRows) { + return nil, errorsx.WithStack(fosite.ErrNotFound) + } else if err != nil { + return nil, sqlcon.HandleError(err) + } + + fositeRequest, err := r.toRequest(ctx, session, p) + if err != nil { + return nil, err + } + + if r.Active { + return fositeRequest, nil + } + + if gracePeriod := p.r.Config().RefreshTokenRotationGracePeriod(ctx); gracePeriod > 0 && r.FirstUsedAt.Valid { + if r.FirstUsedAt.Time.Add(gracePeriod).Before(time.Now()) { + return fositeRequest, errors.WithStack(fosite.ErrInactiveToken) + } + + r.Active = true // We set active to true because we are in the grace period. + return r.toRequest(ctx, session, p) // And re-generate the request + } + + return fositeRequest, errors.WithStack(fosite.ErrInactiveToken) } func (p *Persister) DeleteRefreshTokenSession(ctx context.Context, signature string) (err error) { @@ -496,7 +549,17 @@ func (p *Persister) RevokeRefreshToken(ctx context.Context, id string) (err erro func (p *Persister) RevokeRefreshTokenMaybeGracePeriod(ctx context.Context, id string, _ string) (err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.RevokeRefreshTokenMaybeGracePeriod") defer otelx.End(span, &err) - return p.deactivateSessionByRequestID(ctx, id, sqlTableRefresh) + + /* #nosec G201 table is static */ + return sqlcon.HandleError( + p.Connection(ctx). + RawQuery( + fmt.Sprintf("UPDATE %s SET active=false, first_used_at = CURRENT_TIMESTAMP WHERE request_id=? AND nid = ? AND active", OAuth2RequestSQL{Table: sqlTableRefresh}.TableName()), + id, + p.NetworkID(ctx), + ). + Exec(), + ) } func (p *Persister) RevokeAccessToken(ctx context.Context, id string) (err error) { diff --git a/scripts/db-diff.sh b/scripts/db-diff.sh index 61ce4993edc..52e77387ff7 100755 --- a/scripts/db-diff.sh +++ b/scripts/db-diff.sh @@ -81,7 +81,7 @@ function dump_pg { make test-resetdb >/dev/null 2>&1 sleep 4 - go run . migrate sql "$TEST_DATABASE_POSTGRESQL" --yes >&2 || true + go run . migrate sql up "$TEST_DATABASE_POSTGRESQL" --yes >&2 || true sleep 1 pg_dump -s "$TEST_DATABASE_POSTGRESQL" | sed '/^--/d' } @@ -94,7 +94,7 @@ function dump_cockroach { make test-resetdb >/dev/null 2>&1 sleep 4 - go run . migrate sql "$TEST_DATABASE_COCKROACHDB" --yes > /dev/null || true + go run . migrate sql up "$TEST_DATABASE_COCKROACHDB" --yes > /dev/null || true hydra::util::parse-connection-url "${TEST_DATABASE_COCKROACHDB}" docker run --rm --net=host -it cockroachdb/cockroach:latest-v24.1 dump --dump-all --dump-mode=schema --insecure --user="${DB_USER}" --host="${DB_HOST}" --port="${DB_PORT}" } @@ -107,7 +107,7 @@ function dump_sqlite { hydra::util::ensure-sqlite rm "$SQLITE_PATH" > /dev/null 2>&1 || true - go run -tags sqlite,sqlite_omit_load_extension . migrate sql "sqlite://$SQLITE_PATH?_fk=true" --yes > /dev/null 2>&1 || true + go run -tags sqlite,sqlite_omit_load_extension . migrate sql up "sqlite://$SQLITE_PATH?_fk=true" --yes > /dev/null 2>&1 || true echo '.dump' | sqlite3 "$SQLITE_PATH" } @@ -120,7 +120,7 @@ function dump_mysql { hydra::util::ensure-mysqldump make test-resetdb >/dev/null 2>&1 sleep 10 - go run . migrate sql "$TEST_DATABASE_MYSQL" --yes > /dev/null || true + go run . migrate sql up "$TEST_DATABASE_MYSQL" --yes > /dev/null || true hydra::util::parse-connection-url "${TEST_DATABASE_MYSQL}" mysqldump --user="$DB_USER" --password="$DB_PASSWORD" --host="$DB_HOST" --port="$DB_PORT" "$DB_DB" --no-data } diff --git a/spec/config.json b/spec/config.json index 9899db71df0..72f81534c66 100644 --- a/spec/config.json +++ b/spec/config.json @@ -40,7 +40,7 @@ }, "mode": { "type": "integer", - "description": "Mode of unix socket in numeric form", + "description": "Mode of unix socket in numeric form, base 10.", "default": 493, "minimum": 0, "maximum": 511 @@ -1068,6 +1068,21 @@ "type": "object", "additionalProperties": false, "properties": { + "refresh_token": { + "type": "object", + "properties": { + "grace_period": { + "title": "Refresh Token Rotation Grace Period", + "description": "Configures how long a Refresh Token remains valid after it has been used. The maximum value is one hour.", + "default": "0s", + "allOf": [ + { + "$ref": "#/definitions/duration" + } + ] + } + } + }, "jwt": { "type": "object", "additionalProperties": false, @@ -1122,8 +1137,8 @@ } ] } - } - }, + } + }, "secrets": { "type": "object", "additionalProperties": false, diff --git a/test/e2e/circle-ci.bash b/test/e2e/circle-ci.bash index 1f93dee3b25..03b8f70ac79 100755 --- a/test/e2e/circle-ci.bash +++ b/test/e2e/circle-ci.bash @@ -92,7 +92,7 @@ case $i in esac done -./hydra migrate sql --yes $TEST_DATABASE > ./hydra-migrate.e2e.log 2>&1 +./hydra migrate sql up --yes $TEST_DATABASE > ./hydra-migrate.e2e.log 2>&1 DSN=$TEST_DATABASE \ ./hydra serve all --dev --sqa-opt-out > ./hydra.e2e.log 2>&1 & diff --git a/test/e2e/docker-compose.cockroach.yml b/test/e2e/docker-compose.cockroach.yml index 780e8ffdf2a..de0eee0384c 100644 --- a/test/e2e/docker-compose.cockroach.yml +++ b/test/e2e/docker-compose.cockroach.yml @@ -5,7 +5,7 @@ services: image: oryd/hydra:e2e environment: - DSN=cockroach://root@cockroachd:26257/defaultdb?sslmode=disable&max_conns=20&max_idle_conns=4 - command: migrate sql -e --yes + command: migrate sql up -e --yes restart: on-failure hydra: diff --git a/test/e2e/docker-compose.mysql.yml b/test/e2e/docker-compose.mysql.yml index 5750d3ec81e..e8a03d4eb67 100644 --- a/test/e2e/docker-compose.mysql.yml +++ b/test/e2e/docker-compose.mysql.yml @@ -5,7 +5,7 @@ services: image: oryd/hydra:e2e environment: - DSN=mysql://root:secret@tcp(mysqld:3306)/mysql?max_conns=20&max_idle_conns=4 - command: migrate sql -e --yes + command: migrate sql up -e --yes restart: on-failure hydra: diff --git a/test/e2e/docker-compose.postgres.yml b/test/e2e/docker-compose.postgres.yml index 7c1a4f6bee2..72e3ed7443c 100644 --- a/test/e2e/docker-compose.postgres.yml +++ b/test/e2e/docker-compose.postgres.yml @@ -5,7 +5,7 @@ services: image: oryd/hydra:e2e environment: - DSN=postgres://hydra:secret@postgresd:5432/hydra?sslmode=disable&max_conns=20&max_idle_conns=4 - command: migrate sql -e --yes + command: migrate sql up -e --yes restart: on-failure hydra: diff --git a/x/fosite_storer.go b/x/fosite_storer.go index 23654c519b9..546cfc98870 100644 --- a/x/fosite_storer.go +++ b/x/fosite_storer.go @@ -18,16 +18,13 @@ import ( type FositeStorer interface { fosite.Storage oauth2.CoreStorage + oauth2.TokenRevocationStorage openid.OpenIDConnectRequestStorage pkce.PKCERequestStorage rfc7523.RFC7523KeyStorage verifiable.NonceManager oauth2.ResourceOwnerPasswordCredentialsGrantStorage - RevokeRefreshToken(ctx context.Context, requestID string) error - - RevokeAccessToken(ctx context.Context, requestID string) error - // flush the access token requests from the database. // no data will be deleted after the 'notAfter' timeframe. FlushInactiveAccessTokens(ctx context.Context, notAfter time.Time, limit int, batchSize int) error diff --git a/x/int_to_bytes.go b/x/int_to_bytes.go index 4cb8c9e8caf..08805ae4de2 100644 --- a/x/int_to_bytes.go +++ b/x/int_to_bytes.go @@ -12,7 +12,7 @@ import ( // IntToBytes converts an int64 to a byte slice. It is the inverse of BytesToInt. func IntToBytes(i int64) []byte { b := make([]byte, 8) - binary.LittleEndian.PutUint64(b, uint64(i)) + binary.LittleEndian.PutUint64(b, uint64(i)) //nolint:gosec return b } @@ -22,5 +22,5 @@ func BytesToInt(b []byte) (int64, error) { if len(b) != 8 { return 0, errors.New("byte slice must be 8 bytes long") } - return int64(binary.LittleEndian.Uint64(b)), nil + return int64(binary.LittleEndian.Uint64(b)), nil //nolint:gosec }