├── .editorconfig ├── .github └── workflows │ ├── issues.yml │ ├── security.yml │ ├── test.yml │ └── verify.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cookie.go ├── cookie_test.go ├── doc.go ├── go.mod ├── go.sum ├── lex.go ├── options.go ├── sessions.go ├── sessions_test.go ├── store.go └── store_test.go /.editorconfig: -------------------------------------------------------------------------------- 1 | ; https://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | insert_final_newline = true 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [{Makefile,go.mod,go.sum,*.go,.gitmodules}] 13 | indent_style = tab 14 | indent_size = 4 15 | 16 | [*.md] 17 | indent_size = 4 18 | trim_trailing_whitespace = false 19 | 20 | eclint_indent_style = unset 21 | -------------------------------------------------------------------------------- /.github/workflows/issues.yml: -------------------------------------------------------------------------------- 1 | # Add all the issues created to the project. 2 | name: Add issue or pull request to Project 3 | 4 | on: 5 | issues: 6 | types: 7 | - opened 8 | pull_request_target: 9 | types: 10 | - opened 11 | - reopened 12 | 13 | jobs: 14 | add-to-project: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Add issue to project 18 | uses: actions/add-to-project@v0.5.0 19 | with: 20 | project-url: https://github.com/orgs/gorilla/projects/4 21 | github-token: ${{ secrets.ADD_TO_PROJECT_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: Security 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | permissions: 10 | contents: read 11 | jobs: 12 | scan: 13 | strategy: 14 | matrix: 15 | go: ['1.23'] 16 | fail-fast: true 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout Code 20 | uses: actions/checkout@v3 21 | 22 | - name: Setup Go ${{ matrix.go }} 23 | uses: actions/setup-go@v4 24 | with: 25 | go-version: ${{ matrix.go }} 26 | cache: false 27 | 28 | - name: Run GoSec 29 | uses: securego/gosec@master 30 | with: 31 | args: -exclude-dir examples ./... 32 | 33 | - name: Run GoVulnCheck 34 | uses: golang/govulncheck-action@v1 35 | with: 36 | go-version-input: ${{ matrix.go }} 37 | go-package: ./... 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | permissions: 10 | contents: read 11 | jobs: 12 | unit: 13 | strategy: 14 | matrix: 15 | go: ['1.23'] 16 | os: [ubuntu-latest, macos-latest, windows-latest] 17 | fail-fast: true 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - name: Checkout Code 21 | uses: actions/checkout@v3 22 | 23 | - name: Setup Go ${{ matrix.go }} 24 | uses: actions/setup-go@v4 25 | with: 26 | go-version: ${{ matrix.go }} 27 | cache: false 28 | 29 | - name: Run Tests 30 | run: go test -race -cover -coverprofile=coverage -covermode=atomic -v ./... 31 | 32 | - name: Upload coverage to Codecov 33 | uses: codecov/codecov-action@v3 34 | with: 35 | files: ./coverage 36 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: Verify 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | permissions: 10 | contents: read 11 | jobs: 12 | lint: 13 | strategy: 14 | matrix: 15 | go: ['1.23'] 16 | fail-fast: true 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout Code 20 | uses: actions/checkout@v3 21 | 22 | - name: Setup Go ${{ matrix.go }} 23 | uses: actions/setup-go@v4 24 | with: 25 | go-version: ${{ matrix.go }} 26 | cache: false 27 | 28 | - name: Run GolangCI-Lint 29 | uses: golangci/golangci-lint-action@v3 30 | with: 31 | version: v1.60.1 32 | args: --timeout=5m 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.coverprofile 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 The Gorilla Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO_LINT=$(shell which golangci-lint 2> /dev/null || echo '') 2 | GO_LINT_URI=github.com/golangci/golangci-lint/cmd/golangci-lint@latest 3 | 4 | GO_SEC=$(shell which gosec 2> /dev/null || echo '') 5 | GO_SEC_URI=github.com/securego/gosec/v2/cmd/gosec@latest 6 | 7 | GO_VULNCHECK=$(shell which govulncheck 2> /dev/null || echo '') 8 | GO_VULNCHECK_URI=golang.org/x/vuln/cmd/govulncheck@latest 9 | 10 | .PHONY: golangci-lint 11 | golangci-lint: 12 | $(if $(GO_LINT), ,go install $(GO_LINT_URI)) 13 | @echo "##### Running golangci-lint" 14 | golangci-lint run -v 15 | 16 | .PHONY: gosec 17 | gosec: 18 | $(if $(GO_SEC), ,go install $(GO_SEC_URI)) 19 | @echo "##### Running gosec" 20 | gosec ./... 21 | 22 | .PHONY: govulncheck 23 | govulncheck: 24 | $(if $(GO_VULNCHECK), ,go install $(GO_VULNCHECK_URI)) 25 | @echo "##### Running govulncheck" 26 | govulncheck ./... 27 | 28 | .PHONY: verify 29 | verify: golangci-lint gosec govulncheck 30 | 31 | .PHONY: test 32 | test: 33 | @echo "##### Running tests" 34 | go test -race -cover -coverprofile=coverage.coverprofile -covermode=atomic -v ./... 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gorilla Sessions 2 | 3 | > [!IMPORTANT] 4 | > The latest version of this repository requires go 1.23 because of the new partitioned attribute. The last version that is compatible with older versions of go is v1.3.0. 5 | 6 | ![testing](https://github.com/gorilla/sessions/actions/workflows/test.yml/badge.svg) 7 | [![codecov](https://codecov.io/github/gorilla/sessions/branch/main/graph/badge.svg)](https://codecov.io/github/gorilla/sessions) 8 | [![godoc](https://godoc.org/github.com/gorilla/sessions?status.svg)](https://godoc.org/github.com/gorilla/sessions) 9 | [![sourcegraph](https://sourcegraph.com/github.com/gorilla/sessions/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/sessions?badge) 10 | 11 | ![Gorilla Logo](https://github.com/gorilla/.github/assets/53367916/d92caabf-98e0-473e-bfbf-ab554ba435e5) 12 | 13 | gorilla/sessions provides cookie and filesystem sessions and infrastructure for 14 | custom session backends. 15 | 16 | The key features are: 17 | 18 | - Simple API: use it as an easy way to set signed (and optionally 19 | encrypted) cookies. 20 | - Built-in backends to store sessions in cookies or the filesystem. 21 | - Flash messages: session values that last until read. 22 | - Convenient way to switch session persistency (aka "remember me") and set 23 | other attributes. 24 | - Mechanism to rotate authentication and encryption keys. 25 | - Multiple sessions per request, even using different backends. 26 | - Interfaces and infrastructure for custom session backends: sessions from 27 | different stores can be retrieved and batch-saved using a common API. 28 | 29 | Let's start with an example that shows the sessions API in a nutshell: 30 | 31 | ```go 32 | import ( 33 | "net/http" 34 | "github.com/gorilla/sessions" 35 | ) 36 | 37 | // Note: Don't store your key in your source code. Pass it via an 38 | // environmental variable, or flag (or both), and don't accidentally commit it 39 | // alongside your code. Ensure your key is sufficiently random - i.e. use Go's 40 | // crypto/rand or securecookie.GenerateRandomKey(32) and persist the result. 41 | var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY"))) 42 | 43 | func MyHandler(w http.ResponseWriter, r *http.Request) { 44 | // Get a session. We're ignoring the error resulted from decoding an 45 | // existing session: Get() always returns a session, even if empty. 46 | session, _ := store.Get(r, "session-name") 47 | // Set some session values. 48 | session.Values["foo"] = "bar" 49 | session.Values[42] = 43 50 | // Save it before we write to the response/return from the handler. 51 | err := session.Save(r, w) 52 | if err != nil { 53 | http.Error(w, err.Error(), http.StatusInternalServerError) 54 | return 55 | } 56 | } 57 | ``` 58 | 59 | First we initialize a session store calling `NewCookieStore()` and passing a 60 | secret key used to authenticate the session. Inside the handler, we call 61 | `store.Get()` to retrieve an existing session or create a new one. Then we set 62 | some session values in session.Values, which is a `map[interface{}]interface{}`. 63 | And finally we call `session.Save()` to save the session in the response. 64 | 65 | More examples are available at [package documentation](https://pkg.go.dev/github.com/gorilla/sessions). 66 | 67 | ## Store Implementations 68 | 69 | Other implementations of the `sessions.Store` interface: 70 | 71 | - [github.com/starJammer/gorilla-sessions-arangodb](https://github.com/starJammer/gorilla-sessions-arangodb) - ArangoDB 72 | - [github.com/yosssi/boltstore](https://github.com/yosssi/boltstore) - Bolt 73 | - [github.com/srinathgs/couchbasestore](https://github.com/srinathgs/couchbasestore) - Couchbase 74 | - [github.com/denizeren/dynamostore](https://github.com/denizeren/dynamostore) - Dynamodb on AWS 75 | - [github.com/savaki/dynastore](https://github.com/savaki/dynastore) - DynamoDB on AWS (Official AWS library) 76 | - [github.com/bradleypeabody/gorilla-sessions-memcache](https://github.com/bradleypeabody/gorilla-sessions-memcache) - Memcache 77 | - [github.com/dsoprea/go-appengine-sessioncascade](https://github.com/dsoprea/go-appengine-sessioncascade) - Memcache/Datastore/Context in AppEngine 78 | - [github.com/kidstuff/mongostore](https://github.com/kidstuff/mongostore) - MongoDB 79 | - [github.com/srinathgs/mysqlstore](https://github.com/srinathgs/mysqlstore) - MySQL 80 | - [github.com/danielepintore/gorilla-sessions-mysql](https://github.com/danielepintore/gorilla-sessions-mysql) - MySQL 81 | - [github.com/EnumApps/clustersqlstore](https://github.com/EnumApps/clustersqlstore) - MySQL Cluster 82 | - [github.com/antonlindstrom/pgstore](https://github.com/antonlindstrom/pgstore) - PostgreSQL 83 | - [github.com/boj/redistore](https://github.com/boj/redistore) - Redis 84 | - [github.com/rbcervilla/redisstore](https://github.com/rbcervilla/redisstore) - Redis (Single, Sentinel, Cluster) 85 | - [github.com/boj/rethinkstore](https://github.com/boj/rethinkstore) - RethinkDB 86 | - [github.com/boj/riakstore](https://github.com/boj/riakstore) - Riak 87 | - [github.com/michaeljs1990/sqlitestore](https://github.com/michaeljs1990/sqlitestore) - SQLite 88 | - [github.com/wader/gormstore](https://github.com/wader/gormstore) - GORM (MySQL, PostgreSQL, SQLite) 89 | - [github.com/gernest/qlstore](https://github.com/gernest/qlstore) - ql 90 | - [github.com/quasoft/memstore](https://github.com/quasoft/memstore) - In-memory implementation for use in unit tests 91 | - [github.com/lafriks/xormstore](https://github.com/lafriks/xormstore) - XORM (MySQL, PostgreSQL, SQLite, Microsoft SQL Server, TiDB) 92 | - [github.com/GoogleCloudPlatform/firestore-gorilla-sessions](https://github.com/GoogleCloudPlatform/firestore-gorilla-sessions) - Cloud Firestore 93 | - [github.com/stephenafamo/crdbstore](https://github.com/stephenafamo/crdbstore) - CockroachDB 94 | - [github.com/ryicoh/tikvstore](github.com/ryicoh/tikvstore) - TiKV 95 | 96 | ## License 97 | 98 | BSD licensed. See the LICENSE file for details. 99 | -------------------------------------------------------------------------------- /cookie.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package sessions 6 | 7 | import "net/http" 8 | 9 | // newCookieFromOptions returns an http.Cookie with the options set. 10 | func newCookieFromOptions(name, value string, options *Options) *http.Cookie { 11 | return &http.Cookie{ 12 | Name: name, 13 | Value: value, 14 | Path: options.Path, 15 | Domain: options.Domain, 16 | MaxAge: options.MaxAge, 17 | Secure: options.Secure, 18 | HttpOnly: options.HttpOnly, 19 | Partitioned: options.Partitioned, 20 | SameSite: options.SameSite, 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /cookie_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package sessions 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | // Test for creating new http.Cookie from name, value and options 12 | func TestNewCookieFromOptions(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | value string 16 | path string 17 | domain string 18 | maxAge int 19 | secure bool 20 | httpOnly bool 21 | partitioned bool 22 | }{ 23 | {"", "bar", "/foo/bar", "foo.example.com", 3600, true, true, true}, 24 | {"foo", "", "/foo/bar", "foo.example.com", 3600, true, true, true}, 25 | {"foo", "bar", "", "foo.example.com", 3600, true, true, true}, 26 | {"foo", "bar", "/foo/bar", "", 3600, true, true, true}, 27 | {"foo", "bar", "/foo/bar", "foo.example.com", 0, true, true, true}, 28 | {"foo", "bar", "/foo/bar", "foo.example.com", 3600, false, true, true}, 29 | {"foo", "bar", "/foo/bar", "foo.example.com", 3600, true, false, true}, 30 | {"foo", "bar", "/foo/bar", "foo.example.com", 3600, true, true, false}, 31 | } 32 | for i, v := range tests { 33 | options := &Options{ 34 | Path: v.path, 35 | Domain: v.domain, 36 | MaxAge: v.maxAge, 37 | Secure: v.secure, 38 | HttpOnly: v.httpOnly, 39 | Partitioned: v.partitioned, 40 | } 41 | cookie := newCookieFromOptions(v.name, v.value, options) 42 | if cookie.Name != v.name { 43 | t.Fatalf("%v: bad cookie name: got %q, want %q", i+1, cookie.Name, v.name) 44 | } 45 | if cookie.Value != v.value { 46 | t.Fatalf("%v: bad cookie value: got %q, want %q", i+1, cookie.Value, v.value) 47 | } 48 | if cookie.Path != v.path { 49 | t.Fatalf("%v: bad cookie path: got %q, want %q", i+1, cookie.Path, v.path) 50 | } 51 | if cookie.Domain != v.domain { 52 | t.Fatalf("%v: bad cookie domain: got %q, want %q", i+1, cookie.Domain, v.domain) 53 | } 54 | if cookie.MaxAge != v.maxAge { 55 | t.Fatalf("%v: bad cookie maxAge: got %q, want %q", i+1, cookie.MaxAge, v.maxAge) 56 | } 57 | if cookie.Secure != v.secure { 58 | t.Fatalf("%v: bad cookie secure: got %v, want %v", i+1, cookie.Secure, v.secure) 59 | } 60 | if cookie.HttpOnly != v.httpOnly { 61 | t.Fatalf("%v: bad cookie httpOnly: got %v, want %v", i+1, cookie.HttpOnly, v.httpOnly) 62 | } 63 | if cookie.Partitioned != v.partitioned { 64 | t.Fatalf("%v: bad cookie partitioned: got %v, want %v", i+1, cookie.Partitioned, v.partitioned) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package sessions provides cookie and filesystem sessions and 7 | infrastructure for custom session backends. 8 | 9 | The key features are: 10 | 11 | * Simple API: use it as an easy way to set signed (and optionally 12 | encrypted) cookies. 13 | * Built-in backends to store sessions in cookies or the filesystem. 14 | * Flash messages: session values that last until read. 15 | * Convenient way to switch session persistency (aka "remember me") and set 16 | other attributes. 17 | * Mechanism to rotate authentication and encryption keys. 18 | * Multiple sessions per request, even using different backends. 19 | * Interfaces and infrastructure for custom session backends: sessions from 20 | different stores can be retrieved and batch-saved using a common API. 21 | 22 | Let's start with an example that shows the sessions API in a nutshell: 23 | 24 | import ( 25 | "net/http" 26 | "github.com/gorilla/sessions" 27 | ) 28 | 29 | // Note: Don't store your key in your source code. Pass it via an 30 | // environmental variable, or flag (or both), and don't accidentally commit it 31 | // alongside your code. Ensure your key is sufficiently random - i.e. use Go's 32 | // crypto/rand or securecookie.GenerateRandomKey(32) and persist the result. 33 | // Ensure SESSION_KEY exists in the environment, or sessions will fail. 34 | var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY"))) 35 | 36 | func MyHandler(w http.ResponseWriter, r *http.Request) { 37 | // Get a session. Get() always returns a session, even if empty. 38 | session, err := store.Get(r, "session-name") 39 | if err != nil { 40 | http.Error(w, err.Error(), http.StatusInternalServerError) 41 | return 42 | } 43 | 44 | // Set some session values. 45 | session.Values["foo"] = "bar" 46 | session.Values[42] = 43 47 | // Save it before we write to the response/return from the handler. 48 | err = session.Save(r, w) 49 | if err != nil { 50 | http.Error(w, err.Error(), http.StatusInternalServerError) 51 | return 52 | } 53 | } 54 | 55 | First we initialize a session store calling NewCookieStore() and passing a 56 | secret key used to authenticate the session. Inside the handler, we call 57 | store.Get() to retrieve an existing session or a new one. Then we set some 58 | session values in session.Values, which is a map[interface{}]interface{}. 59 | And finally we call session.Save() to save the session in the response. 60 | 61 | Note that in production code, we should check for errors when calling 62 | session.Save(r, w), and either display an error message or otherwise handle it. 63 | 64 | Save must be called before writing to the response, otherwise the session 65 | cookie will not be sent to the client. 66 | 67 | That's all you need to know for the basic usage. Let's take a look at other 68 | options, starting with flash messages. 69 | 70 | Flash messages are session values that last until read. The term appeared with 71 | Ruby On Rails a few years back. When we request a flash message, it is removed 72 | from the session. To add a flash, call session.AddFlash(), and to get all 73 | flashes, call session.Flashes(). Here is an example: 74 | 75 | func MyHandler(w http.ResponseWriter, r *http.Request) { 76 | // Get a session. 77 | session, err := store.Get(r, "session-name") 78 | if err != nil { 79 | http.Error(w, err.Error(), http.StatusInternalServerError) 80 | return 81 | } 82 | 83 | // Get the previous flashes, if any. 84 | if flashes := session.Flashes(); len(flashes) > 0 { 85 | // Use the flash values. 86 | } else { 87 | // Set a new flash. 88 | session.AddFlash("Hello, flash messages world!") 89 | } 90 | err = session.Save(r, w) 91 | if err != nil { 92 | http.Error(w, err.Error(), http.StatusInternalServerError) 93 | return 94 | } 95 | } 96 | 97 | Flash messages are useful to set information to be read after a redirection, 98 | like after form submissions. 99 | 100 | There may also be cases where you want to store a complex datatype within a 101 | session, such as a struct. Sessions are serialised using the encoding/gob package, 102 | so it is easy to register new datatypes for storage in sessions: 103 | 104 | import( 105 | "encoding/gob" 106 | "github.com/gorilla/sessions" 107 | ) 108 | 109 | type Person struct { 110 | FirstName string 111 | LastName string 112 | Email string 113 | Age int 114 | } 115 | 116 | type M map[string]interface{} 117 | 118 | func init() { 119 | 120 | gob.Register(&Person{}) 121 | gob.Register(&M{}) 122 | } 123 | 124 | As it's not possible to pass a raw type as a parameter to a function, gob.Register() 125 | relies on us passing it a value of the desired type. In the example above we've passed 126 | it a pointer to a struct and a pointer to a custom type representing a 127 | map[string]interface. (We could have passed non-pointer values if we wished.) This will 128 | then allow us to serialise/deserialise values of those types to and from our sessions. 129 | 130 | Note that because session values are stored in a map[string]interface{}, there's 131 | a need to type-assert data when retrieving it. We'll use the Person struct we registered above: 132 | 133 | func MyHandler(w http.ResponseWriter, r *http.Request) { 134 | session, err := store.Get(r, "session-name") 135 | if err != nil { 136 | http.Error(w, err.Error(), http.StatusInternalServerError) 137 | return 138 | } 139 | 140 | // Retrieve our struct and type-assert it 141 | val := session.Values["person"] 142 | var person = &Person{} 143 | if person, ok := val.(*Person); !ok { 144 | // Handle the case that it's not an expected type 145 | } 146 | 147 | // Now we can use our person object 148 | } 149 | 150 | By default, session cookies last for a month. This is probably too long for 151 | some cases, but it is easy to change this and other attributes during 152 | runtime. Sessions can be configured individually or the store can be 153 | configured and then all sessions saved using it will use that configuration. 154 | We access session.Options or store.Options to set a new configuration. The 155 | fields are basically a subset of http.Cookie fields. Let's change the 156 | maximum age of a session to one week: 157 | 158 | session.Options = &sessions.Options{ 159 | Path: "/", 160 | MaxAge: 86400 * 7, 161 | HttpOnly: true, 162 | } 163 | 164 | Sometimes we may want to change authentication and/or encryption keys without 165 | breaking existing sessions. The CookieStore supports key rotation, and to use 166 | it you just need to set multiple authentication and encryption keys, in pairs, 167 | to be tested in order: 168 | 169 | var store = sessions.NewCookieStore( 170 | []byte("new-authentication-key"), 171 | []byte("new-encryption-key"), 172 | []byte("old-authentication-key"), 173 | []byte("old-encryption-key"), 174 | ) 175 | 176 | New sessions will be saved using the first pair. Old sessions can still be 177 | read because the first pair will fail, and the second will be tested. This 178 | makes it easy to "rotate" secret keys and still be able to validate existing 179 | sessions. Note: for all pairs the encryption key is optional; set it to nil 180 | or omit it and and encryption won't be used. 181 | 182 | Multiple sessions can be used in the same request, even with different 183 | session backends. When this happens, calling Save() on each session 184 | individually would be cumbersome, so we have a way to save all sessions 185 | at once: it's sessions.Save(). Here's an example: 186 | 187 | var store = sessions.NewCookieStore([]byte("something-very-secret")) 188 | 189 | func MyHandler(w http.ResponseWriter, r *http.Request) { 190 | // Get a session and set a value. 191 | session1, _ := store.Get(r, "session-one") 192 | session1.Values["foo"] = "bar" 193 | // Get another session and set another value. 194 | session2, _ := store.Get(r, "session-two") 195 | session2.Values[42] = 43 196 | // Save all sessions. 197 | err = sessions.Save(r, w) 198 | if err != nil { 199 | http.Error(w, err.Error(), http.StatusInternalServerError) 200 | return 201 | } 202 | } 203 | 204 | This is possible because when we call Get() from a session store, it adds the 205 | session to a common registry. Save() uses it to save all registered sessions. 206 | */ 207 | package sessions 208 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gorilla/sessions 2 | 3 | go 1.23 4 | 5 | require github.com/gorilla/securecookie v1.1.2 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 2 | github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 3 | github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 4 | -------------------------------------------------------------------------------- /lex.go: -------------------------------------------------------------------------------- 1 | // This file contains code adapted from the Go standard library 2 | // https://github.com/golang/go/blob/39ad0fd0789872f9469167be7fe9578625ff246e/src/net/http/lex.go 3 | 4 | package sessions 5 | 6 | import "strings" 7 | 8 | var isTokenTable = [127]bool{ 9 | '!': true, 10 | '#': true, 11 | '$': true, 12 | '%': true, 13 | '&': true, 14 | '\'': true, 15 | '*': true, 16 | '+': true, 17 | '-': true, 18 | '.': true, 19 | '0': true, 20 | '1': true, 21 | '2': true, 22 | '3': true, 23 | '4': true, 24 | '5': true, 25 | '6': true, 26 | '7': true, 27 | '8': true, 28 | '9': true, 29 | 'A': true, 30 | 'B': true, 31 | 'C': true, 32 | 'D': true, 33 | 'E': true, 34 | 'F': true, 35 | 'G': true, 36 | 'H': true, 37 | 'I': true, 38 | 'J': true, 39 | 'K': true, 40 | 'L': true, 41 | 'M': true, 42 | 'N': true, 43 | 'O': true, 44 | 'P': true, 45 | 'Q': true, 46 | 'R': true, 47 | 'S': true, 48 | 'T': true, 49 | 'U': true, 50 | 'W': true, 51 | 'V': true, 52 | 'X': true, 53 | 'Y': true, 54 | 'Z': true, 55 | '^': true, 56 | '_': true, 57 | '`': true, 58 | 'a': true, 59 | 'b': true, 60 | 'c': true, 61 | 'd': true, 62 | 'e': true, 63 | 'f': true, 64 | 'g': true, 65 | 'h': true, 66 | 'i': true, 67 | 'j': true, 68 | 'k': true, 69 | 'l': true, 70 | 'm': true, 71 | 'n': true, 72 | 'o': true, 73 | 'p': true, 74 | 'q': true, 75 | 'r': true, 76 | 's': true, 77 | 't': true, 78 | 'u': true, 79 | 'v': true, 80 | 'w': true, 81 | 'x': true, 82 | 'y': true, 83 | 'z': true, 84 | '|': true, 85 | '~': true, 86 | } 87 | 88 | func isToken(r rune) bool { 89 | i := int(r) 90 | return i < len(isTokenTable) && isTokenTable[i] 91 | } 92 | 93 | func isNotToken(r rune) bool { 94 | return !isToken(r) 95 | } 96 | 97 | func isCookieNameValid(raw string) bool { 98 | if raw == "" { 99 | return false 100 | } 101 | return strings.IndexFunc(raw, isNotToken) < 0 102 | } 103 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package sessions 6 | 7 | import "net/http" 8 | 9 | // Options stores configuration for a session or session store. 10 | // 11 | // Fields are a subset of http.Cookie fields. 12 | type Options struct { 13 | Path string 14 | Domain string 15 | // MaxAge=0 means no Max-Age attribute specified and the cookie will be 16 | // deleted after the browser session ends. 17 | // MaxAge<0 means delete cookie immediately. 18 | // MaxAge>0 means Max-Age attribute present and given in seconds. 19 | MaxAge int 20 | Secure bool 21 | HttpOnly bool 22 | Partitioned bool 23 | SameSite http.SameSite 24 | } 25 | -------------------------------------------------------------------------------- /sessions.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package sessions 6 | 7 | import ( 8 | "context" 9 | "encoding/gob" 10 | "fmt" 11 | "net/http" 12 | "time" 13 | ) 14 | 15 | // Default flashes key. 16 | const flashesKey = "_flash" 17 | 18 | // Session -------------------------------------------------------------------- 19 | 20 | // NewSession is called by session stores to create a new session instance. 21 | func NewSession(store Store, name string) *Session { 22 | return &Session{ 23 | Values: make(map[interface{}]interface{}), 24 | store: store, 25 | name: name, 26 | Options: new(Options), 27 | } 28 | } 29 | 30 | // Session stores the values and optional configuration for a session. 31 | type Session struct { 32 | // The ID of the session, generated by stores. It should not be used for 33 | // user data. 34 | ID string 35 | // Values contains the user-data for the session. 36 | Values map[interface{}]interface{} 37 | Options *Options 38 | IsNew bool 39 | store Store 40 | name string 41 | } 42 | 43 | // Flashes returns a slice of flash messages from the session. 44 | // 45 | // A single variadic argument is accepted, and it is optional: it defines 46 | // the flash key. If not defined "_flash" is used by default. 47 | func (s *Session) Flashes(vars ...string) []interface{} { 48 | var flashes []interface{} 49 | key := flashesKey 50 | if len(vars) > 0 { 51 | key = vars[0] 52 | } 53 | if v, ok := s.Values[key]; ok { 54 | // Drop the flashes and return it. 55 | delete(s.Values, key) 56 | flashes = v.([]interface{}) 57 | } 58 | return flashes 59 | } 60 | 61 | // AddFlash adds a flash message to the session. 62 | // 63 | // A single variadic argument is accepted, and it is optional: it defines 64 | // the flash key. If not defined "_flash" is used by default. 65 | func (s *Session) AddFlash(value interface{}, vars ...string) { 66 | key := flashesKey 67 | if len(vars) > 0 { 68 | key = vars[0] 69 | } 70 | var flashes []interface{} 71 | if v, ok := s.Values[key]; ok { 72 | flashes = v.([]interface{}) 73 | } 74 | s.Values[key] = append(flashes, value) 75 | } 76 | 77 | // Save is a convenience method to save this session. It is the same as calling 78 | // store.Save(request, response, session). You should call Save before writing to 79 | // the response or returning from the handler. 80 | func (s *Session) Save(r *http.Request, w http.ResponseWriter) error { 81 | return s.store.Save(r, w, s) 82 | } 83 | 84 | // Name returns the name used to register the session. 85 | func (s *Session) Name() string { 86 | return s.name 87 | } 88 | 89 | // Store returns the session store used to register the session. 90 | func (s *Session) Store() Store { 91 | return s.store 92 | } 93 | 94 | // Registry ------------------------------------------------------------------- 95 | 96 | // sessionInfo stores a session tracked by the registry. 97 | type sessionInfo struct { 98 | s *Session 99 | e error 100 | } 101 | 102 | // contextKey is the type used to store the registry in the context. 103 | type contextKey int 104 | 105 | // registryKey is the key used to store the registry in the context. 106 | const registryKey contextKey = 0 107 | 108 | // GetRegistry returns a registry instance for the current request. 109 | func GetRegistry(r *http.Request) *Registry { 110 | var ctx = r.Context() 111 | registry := ctx.Value(registryKey) 112 | if registry != nil { 113 | return registry.(*Registry) 114 | } 115 | newRegistry := &Registry{ 116 | request: r, 117 | sessions: make(map[string]sessionInfo), 118 | } 119 | *r = *r.WithContext(context.WithValue(ctx, registryKey, newRegistry)) 120 | return newRegistry 121 | } 122 | 123 | // Registry stores sessions used during a request. 124 | type Registry struct { 125 | request *http.Request 126 | sessions map[string]sessionInfo 127 | } 128 | 129 | // Get registers and returns a session for the given name and session store. 130 | // 131 | // It returns a new session if there are no sessions registered for the name. 132 | func (s *Registry) Get(store Store, name string) (session *Session, err error) { 133 | if !isCookieNameValid(name) { 134 | return nil, fmt.Errorf("sessions: invalid character in cookie name: %s", name) 135 | } 136 | if info, ok := s.sessions[name]; ok { 137 | session, err = info.s, info.e 138 | } else { 139 | session, err = store.New(s.request, name) 140 | session.name = name 141 | s.sessions[name] = sessionInfo{s: session, e: err} 142 | } 143 | session.store = store 144 | return 145 | } 146 | 147 | // Save saves all sessions registered for the current request. 148 | func (s *Registry) Save(w http.ResponseWriter) error { 149 | var errMulti MultiError 150 | for name, info := range s.sessions { 151 | session := info.s 152 | if session.store == nil { 153 | errMulti = append(errMulti, fmt.Errorf( 154 | "sessions: missing store for session %q", name)) 155 | } else if err := session.store.Save(s.request, w, session); err != nil { 156 | errMulti = append(errMulti, fmt.Errorf( 157 | "sessions: error saving session %q -- %v", name, err)) 158 | } 159 | } 160 | if errMulti != nil { 161 | return errMulti 162 | } 163 | return nil 164 | } 165 | 166 | // Helpers -------------------------------------------------------------------- 167 | 168 | func init() { 169 | gob.Register([]interface{}{}) 170 | } 171 | 172 | // Save saves all sessions used during the current request. 173 | func Save(r *http.Request, w http.ResponseWriter) error { 174 | return GetRegistry(r).Save(w) 175 | } 176 | 177 | // NewCookie returns an http.Cookie with the options set. It also sets 178 | // the Expires field calculated based on the MaxAge value, for Internet 179 | // Explorer compatibility. 180 | func NewCookie(name, value string, options *Options) *http.Cookie { 181 | cookie := newCookieFromOptions(name, value, options) 182 | if options.MaxAge > 0 { 183 | d := time.Duration(options.MaxAge) * time.Second 184 | cookie.Expires = time.Now().Add(d) 185 | } else if options.MaxAge < 0 { 186 | // Set it to the past to expire now. 187 | cookie.Expires = time.Unix(1, 0) 188 | } 189 | return cookie 190 | } 191 | 192 | // Error ---------------------------------------------------------------------- 193 | 194 | // MultiError stores multiple errors. 195 | // 196 | // Borrowed from the App Engine SDK. 197 | type MultiError []error 198 | 199 | func (m MultiError) Error() string { 200 | s, n := "", 0 201 | for _, e := range m { 202 | if e != nil { 203 | if n == 0 { 204 | s = e.Error() 205 | } 206 | n++ 207 | } 208 | } 209 | switch n { 210 | case 0: 211 | return "(0 errors)" 212 | case 1: 213 | return s 214 | case 2: 215 | return s + " (and 1 other error)" 216 | } 217 | return fmt.Sprintf("%s (and %d other errors)", s, n-1) 218 | } 219 | -------------------------------------------------------------------------------- /sessions_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package sessions 6 | 7 | import ( 8 | "bytes" 9 | "encoding/gob" 10 | "net/http" 11 | "net/http/httptest" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | // NewRecorder returns an initialized ResponseRecorder. 17 | func NewRecorder() *httptest.ResponseRecorder { 18 | return &httptest.ResponseRecorder{ 19 | HeaderMap: make(http.Header), 20 | Body: new(bytes.Buffer), 21 | } 22 | } 23 | 24 | // ---------------------------------------------------------------------------- 25 | 26 | type FlashMessage struct { 27 | Type int 28 | Message string 29 | } 30 | 31 | func TestFlashes(t *testing.T) { 32 | var req *http.Request 33 | var rsp *httptest.ResponseRecorder 34 | var hdr http.Header 35 | var err error 36 | var ok bool 37 | var cookies []string 38 | var session *Session 39 | var flashes []interface{} 40 | 41 | store := NewCookieStore([]byte("secret-key")) 42 | 43 | if store.Options.SameSite != http.SameSiteNoneMode { 44 | t.Fatalf("cookie store error: default same site is not set to None") 45 | } 46 | 47 | // Round 1 ---------------------------------------------------------------- 48 | 49 | req, _ = http.NewRequest("GET", "http://localhost:8080/", nil) 50 | rsp = NewRecorder() 51 | // Get a session. 52 | if session, err = store.Get(req, "session-key"); err != nil { 53 | t.Fatalf("Error getting session: %v", err) 54 | } 55 | // Get a flash. 56 | flashes = session.Flashes() 57 | if len(flashes) != 0 { 58 | t.Errorf("Expected empty flashes; Got %v", flashes) 59 | } 60 | // Add some flashes. 61 | session.AddFlash("foo") 62 | session.AddFlash("bar") 63 | // Custom key. 64 | session.AddFlash("baz", "custom_key") 65 | // Save. 66 | if err = Save(req, rsp); err != nil { 67 | t.Fatalf("Error saving session: %v", err) 68 | } 69 | hdr = rsp.Header() 70 | cookies, ok = hdr["Set-Cookie"] 71 | if !ok || len(cookies) != 1 { 72 | t.Fatal("No cookies. Header:", hdr) 73 | } 74 | 75 | if !strings.Contains(cookies[0], "SameSite=None") || !strings.Contains(cookies[0], "Secure") { 76 | t.Fatal("Set-Cookie does not contains SameSite=None with Secure, cookie string:", cookies[0]) 77 | } 78 | 79 | if _, err = store.Get(req, "session:key"); err.Error() != "sessions: invalid character in cookie name: session:key" { 80 | t.Fatalf("Expected error due to invalid cookie name") 81 | } 82 | 83 | // Round 2 ---------------------------------------------------------------- 84 | 85 | req, _ = http.NewRequest("GET", "http://localhost:8080/", nil) 86 | req.Header.Add("Cookie", cookies[0]) 87 | // Get a session. 88 | if session, err = store.Get(req, "session-key"); err != nil { 89 | t.Fatalf("Error getting session: %v", err) 90 | } 91 | // Check all saved values. 92 | flashes = session.Flashes() 93 | if len(flashes) != 2 { 94 | t.Fatalf("Expected flashes; Got %v", flashes) 95 | } 96 | if flashes[0] != "foo" || flashes[1] != "bar" { 97 | t.Errorf("Expected foo,bar; Got %v", flashes) 98 | } 99 | flashes = session.Flashes() 100 | if len(flashes) != 0 { 101 | t.Errorf("Expected dumped flashes; Got %v", flashes) 102 | } 103 | // Custom key. 104 | flashes = session.Flashes("custom_key") 105 | if len(flashes) != 1 { 106 | t.Errorf("Expected flashes; Got %v", flashes) 107 | } else if flashes[0] != "baz" { 108 | t.Errorf("Expected baz; Got %v", flashes) 109 | } 110 | flashes = session.Flashes("custom_key") 111 | if len(flashes) != 0 { 112 | t.Errorf("Expected dumped flashes; Got %v", flashes) 113 | } 114 | 115 | // Round 3 ---------------------------------------------------------------- 116 | // Custom type 117 | 118 | req, _ = http.NewRequest("GET", "http://localhost:8080/", nil) 119 | rsp = NewRecorder() 120 | // Get a session. 121 | if session, err = store.Get(req, "session-key"); err != nil { 122 | t.Fatalf("Error getting session: %v", err) 123 | } 124 | // Get a flash. 125 | flashes = session.Flashes() 126 | if len(flashes) != 0 { 127 | t.Errorf("Expected empty flashes; Got %v", flashes) 128 | } 129 | // Add some flashes. 130 | session.AddFlash(&FlashMessage{42, "foo"}) 131 | // Save. 132 | if err = Save(req, rsp); err != nil { 133 | t.Fatalf("Error saving session: %v", err) 134 | } 135 | hdr = rsp.Header() 136 | cookies, ok = hdr["Set-Cookie"] 137 | if !ok || len(cookies) != 1 { 138 | t.Fatal("No cookies. Header:", hdr) 139 | } 140 | 141 | // Round 4 ---------------------------------------------------------------- 142 | // Custom type 143 | 144 | req, _ = http.NewRequest("GET", "http://localhost:8080/", nil) 145 | req.Header.Add("Cookie", cookies[0]) 146 | // Get a session. 147 | if session, err = store.Get(req, "session-key"); err != nil { 148 | t.Fatalf("Error getting session: %v", err) 149 | } 150 | // Check all saved values. 151 | flashes = session.Flashes() 152 | if len(flashes) != 1 { 153 | t.Fatalf("Expected flashes; Got %v", flashes) 154 | } 155 | custom := flashes[0].(FlashMessage) 156 | if custom.Type != 42 || custom.Message != "foo" { 157 | t.Errorf("Expected %#v, got %#v", FlashMessage{42, "foo"}, custom) 158 | } 159 | 160 | // Round 5 ---------------------------------------------------------------- 161 | // Check if a request shallow copy resets the request context data store. 162 | 163 | req, _ = http.NewRequest("GET", "http://localhost:8080/", nil) 164 | 165 | // Get a session. 166 | if session, err = store.Get(req, "session-key"); err != nil { 167 | t.Fatalf("Error getting session: %v", err) 168 | } 169 | 170 | // Put a test value into the session data store. 171 | session.Values["test"] = "test-value" 172 | 173 | // Create a shallow copy of the request. 174 | req = req.WithContext(req.Context()) 175 | 176 | // Get the session again. 177 | if session, err = store.Get(req, "session-key"); err != nil { 178 | t.Fatalf("Error getting session: %v", err) 179 | } 180 | 181 | // Check if the previous inserted value still exists. 182 | if session.Values["test"] == nil { 183 | t.Fatalf("Session test value is lost in the request context!") 184 | } 185 | 186 | // Check if the previous inserted value has the same value. 187 | if session.Values["test"] != "test-value" { 188 | t.Fatalf("Session test value is changed in the request context!") 189 | } 190 | } 191 | 192 | func TestCookieStoreMapPanic(t *testing.T) { 193 | defer func() { 194 | err := recover() 195 | if err != nil { 196 | t.Fatal(err) 197 | } 198 | }() 199 | 200 | store := NewCookieStore([]byte("aaa0defe5d2839cbc46fc4f080cd7adc")) 201 | req, err := http.NewRequest("GET", "http://www.example.com", nil) 202 | if err != nil { 203 | t.Fatal("failed to create request", err) 204 | } 205 | w := httptest.NewRecorder() 206 | 207 | session := NewSession(store, "hello") 208 | 209 | session.Values["data"] = "hello-world" 210 | 211 | err = session.Save(req, w) 212 | if err != nil { 213 | t.Fatal("failed to save session", err) 214 | } 215 | } 216 | 217 | func init() { 218 | gob.Register(FlashMessage{}) 219 | } 220 | -------------------------------------------------------------------------------- /store.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package sessions 6 | 7 | import ( 8 | "encoding/base32" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "sync" 13 | 14 | "github.com/gorilla/securecookie" 15 | ) 16 | 17 | const ( 18 | // File name prefix for session files. 19 | sessionFilePrefix = "session_" 20 | ) 21 | 22 | // Store is an interface for custom session stores. 23 | // 24 | // See CookieStore and FilesystemStore for examples. 25 | type Store interface { 26 | // Get should return a cached session. 27 | Get(r *http.Request, name string) (*Session, error) 28 | 29 | // New should create and return a new session. 30 | // 31 | // Note that New should never return a nil session, even in the case of 32 | // an error if using the Registry infrastructure to cache the session. 33 | New(r *http.Request, name string) (*Session, error) 34 | 35 | // Save should persist session to the underlying store implementation. 36 | Save(r *http.Request, w http.ResponseWriter, s *Session) error 37 | } 38 | 39 | // CookieStore ---------------------------------------------------------------- 40 | 41 | // NewCookieStore returns a new CookieStore. 42 | // 43 | // Keys are defined in pairs to allow key rotation, but the common case is 44 | // to set a single authentication key and optionally an encryption key. 45 | // 46 | // The first key in a pair is used for authentication and the second for 47 | // encryption. The encryption key can be set to nil or omitted in the last 48 | // pair, but the authentication key is required in all pairs. 49 | // 50 | // It is recommended to use an authentication key with 32 or 64 bytes. 51 | // The encryption key, if set, must be either 16, 24, or 32 bytes to select 52 | // AES-128, AES-192, or AES-256 modes. 53 | func NewCookieStore(keyPairs ...[]byte) *CookieStore { 54 | cs := &CookieStore{ 55 | Codecs: securecookie.CodecsFromPairs(keyPairs...), 56 | Options: &Options{ 57 | Path: "/", 58 | MaxAge: 86400 * 30, 59 | SameSite: http.SameSiteNoneMode, 60 | Secure: true, 61 | }, 62 | } 63 | 64 | cs.MaxAge(cs.Options.MaxAge) 65 | return cs 66 | } 67 | 68 | // CookieStore stores sessions using secure cookies. 69 | type CookieStore struct { 70 | Codecs []securecookie.Codec 71 | Options *Options // default configuration 72 | } 73 | 74 | // Get returns a session for the given name after adding it to the registry. 75 | // 76 | // It returns a new session if the sessions doesn't exist. Access IsNew on 77 | // the session to check if it is an existing session or a new one. 78 | // 79 | // It returns a new session and an error if the session exists but could 80 | // not be decoded. 81 | func (s *CookieStore) Get(r *http.Request, name string) (*Session, error) { 82 | return GetRegistry(r).Get(s, name) 83 | } 84 | 85 | // New returns a session for the given name without adding it to the registry. 86 | // 87 | // The difference between New() and Get() is that calling New() twice will 88 | // decode the session data twice, while Get() registers and reuses the same 89 | // decoded session after the first call. 90 | func (s *CookieStore) New(r *http.Request, name string) (*Session, error) { 91 | session := NewSession(s, name) 92 | opts := *s.Options 93 | session.Options = &opts 94 | session.IsNew = true 95 | var err error 96 | if c, errCookie := r.Cookie(name); errCookie == nil { 97 | err = securecookie.DecodeMulti(name, c.Value, &session.Values, 98 | s.Codecs...) 99 | if err == nil { 100 | session.IsNew = false 101 | } 102 | } 103 | return session, err 104 | } 105 | 106 | // Save adds a single session to the response. 107 | func (s *CookieStore) Save(r *http.Request, w http.ResponseWriter, 108 | session *Session) error { 109 | encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, 110 | s.Codecs...) 111 | if err != nil { 112 | return err 113 | } 114 | http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options)) 115 | return nil 116 | } 117 | 118 | // MaxAge sets the maximum age for the store and the underlying cookie 119 | // implementation. Individual sessions can be deleted by setting Options.MaxAge 120 | // = -1 for that session. 121 | func (s *CookieStore) MaxAge(age int) { 122 | s.Options.MaxAge = age 123 | 124 | // Set the maxAge for each securecookie instance. 125 | for _, codec := range s.Codecs { 126 | if sc, ok := codec.(*securecookie.SecureCookie); ok { 127 | sc.MaxAge(age) 128 | } 129 | } 130 | } 131 | 132 | // FilesystemStore ------------------------------------------------------------ 133 | 134 | var fileMutex sync.RWMutex 135 | 136 | // NewFilesystemStore returns a new FilesystemStore. 137 | // 138 | // The path argument is the directory where sessions will be saved. If empty 139 | // it will use os.TempDir(). 140 | // 141 | // See NewCookieStore() for a description of the other parameters. 142 | func NewFilesystemStore(path string, keyPairs ...[]byte) *FilesystemStore { 143 | if path == "" { 144 | path = os.TempDir() 145 | } 146 | fs := &FilesystemStore{ 147 | Codecs: securecookie.CodecsFromPairs(keyPairs...), 148 | Options: &Options{ 149 | Path: "/", 150 | MaxAge: 86400 * 30, 151 | }, 152 | path: path, 153 | } 154 | 155 | fs.MaxAge(fs.Options.MaxAge) 156 | return fs 157 | } 158 | 159 | // FilesystemStore stores sessions in the filesystem. 160 | // 161 | // It also serves as a reference for custom stores. 162 | // 163 | // This store is still experimental and not well tested. Feedback is welcome. 164 | type FilesystemStore struct { 165 | Codecs []securecookie.Codec 166 | Options *Options // default configuration 167 | path string 168 | } 169 | 170 | // MaxLength restricts the maximum length of new sessions to l. 171 | // If l is 0 there is no limit to the size of a session, use with caution. 172 | // The default for a new FilesystemStore is 4096. 173 | func (s *FilesystemStore) MaxLength(l int) { 174 | for _, c := range s.Codecs { 175 | if codec, ok := c.(*securecookie.SecureCookie); ok { 176 | codec.MaxLength(l) 177 | } 178 | } 179 | } 180 | 181 | // Get returns a session for the given name after adding it to the registry. 182 | // 183 | // See CookieStore.Get(). 184 | func (s *FilesystemStore) Get(r *http.Request, name string) (*Session, error) { 185 | return GetRegistry(r).Get(s, name) 186 | } 187 | 188 | // New returns a session for the given name without adding it to the registry. 189 | // 190 | // See CookieStore.New(). 191 | func (s *FilesystemStore) New(r *http.Request, name string) (*Session, error) { 192 | session := NewSession(s, name) 193 | opts := *s.Options 194 | session.Options = &opts 195 | session.IsNew = true 196 | var err error 197 | if c, errCookie := r.Cookie(name); errCookie == nil { 198 | err = securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...) 199 | if err == nil { 200 | err = s.load(session) 201 | if err == nil { 202 | session.IsNew = false 203 | } 204 | } 205 | } 206 | return session, err 207 | } 208 | 209 | var base32RawStdEncoding = base32.StdEncoding.WithPadding(base32.NoPadding) 210 | 211 | // Save adds a single session to the response. 212 | // 213 | // If the Options.MaxAge of the session is <= 0 then the session file will be 214 | // deleted from the store path. With this process it enforces the properly 215 | // session cookie handling so no need to trust in the cookie management in the 216 | // web browser. 217 | func (s *FilesystemStore) Save(r *http.Request, w http.ResponseWriter, 218 | session *Session) error { 219 | // Delete if max-age is <= 0 220 | if session.Options.MaxAge <= 0 { 221 | if err := s.erase(session); err != nil && !os.IsNotExist(err) { 222 | return err 223 | } 224 | http.SetCookie(w, NewCookie(session.Name(), "", session.Options)) 225 | return nil 226 | } 227 | 228 | if session.ID == "" { 229 | // Because the ID is used in the filename, encode it to 230 | // use alphanumeric characters only. 231 | session.ID = base32RawStdEncoding.EncodeToString( 232 | securecookie.GenerateRandomKey(32)) 233 | } 234 | if err := s.save(session); err != nil { 235 | return err 236 | } 237 | encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, 238 | s.Codecs...) 239 | if err != nil { 240 | return err 241 | } 242 | http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options)) 243 | return nil 244 | } 245 | 246 | // MaxAge sets the maximum age for the store and the underlying cookie 247 | // implementation. Individual sessions can be deleted by setting Options.MaxAge 248 | // = -1 for that session. 249 | func (s *FilesystemStore) MaxAge(age int) { 250 | s.Options.MaxAge = age 251 | 252 | // Set the maxAge for each securecookie instance. 253 | for _, codec := range s.Codecs { 254 | if sc, ok := codec.(*securecookie.SecureCookie); ok { 255 | sc.MaxAge(age) 256 | } 257 | } 258 | } 259 | 260 | // save writes encoded session.Values to a file. 261 | func (s *FilesystemStore) save(session *Session) error { 262 | encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, 263 | s.Codecs...) 264 | if err != nil { 265 | return err 266 | } 267 | filename := filepath.Join(s.path, sessionFilePrefix+filepath.Base(session.ID)) 268 | fileMutex.Lock() 269 | defer fileMutex.Unlock() 270 | return os.WriteFile(filename, []byte(encoded), 0600) 271 | } 272 | 273 | // load reads a file and decodes its content into session.Values. 274 | func (s *FilesystemStore) load(session *Session) error { 275 | filename := filepath.Join(s.path, sessionFilePrefix+filepath.Base(session.ID)) 276 | fileMutex.RLock() 277 | defer fileMutex.RUnlock() 278 | fdata, err := os.ReadFile(filepath.Clean(filename)) 279 | if err != nil { 280 | return err 281 | } 282 | if err = securecookie.DecodeMulti(session.Name(), string(fdata), 283 | &session.Values, s.Codecs...); err != nil { 284 | return err 285 | } 286 | return nil 287 | } 288 | 289 | // delete session file 290 | func (s *FilesystemStore) erase(session *Session) error { 291 | filename := filepath.Join(s.path, sessionFilePrefix+filepath.Base(session.ID)) 292 | 293 | fileMutex.RLock() 294 | defer fileMutex.RUnlock() 295 | 296 | err := os.Remove(filename) 297 | return err 298 | } 299 | -------------------------------------------------------------------------------- /store_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package sessions 6 | 7 | import ( 8 | "encoding/base64" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | ) 13 | 14 | // Test for GH-8 for CookieStore 15 | func TestGH8CookieStore(t *testing.T) { 16 | originalPath := "/" 17 | store := NewCookieStore() 18 | store.Options.Path = originalPath 19 | req, err := http.NewRequest("GET", "http://www.example.com", nil) 20 | if err != nil { 21 | t.Fatal("failed to create request", err) 22 | } 23 | 24 | session, err := store.New(req, "hello") 25 | if err != nil { 26 | t.Fatal("failed to create session", err) 27 | } 28 | 29 | store.Options.Path = "/foo" 30 | if session.Options.Path != originalPath { 31 | t.Fatalf("bad session path: got %q, want %q", session.Options.Path, originalPath) 32 | } 33 | } 34 | 35 | // Test for GH-8 for FilesystemStore 36 | func TestGH8FilesystemStore(t *testing.T) { 37 | originalPath := "/" 38 | store := NewFilesystemStore("") 39 | store.Options.Path = originalPath 40 | req, err := http.NewRequest("GET", "http://www.example.com", nil) 41 | if err != nil { 42 | t.Fatal("failed to create request", err) 43 | } 44 | 45 | session, err := store.New(req, "hello") 46 | if err != nil { 47 | t.Fatal("failed to create session", err) 48 | } 49 | 50 | store.Options.Path = "/foo" 51 | if session.Options.Path != originalPath { 52 | t.Fatalf("bad session path: got %q, want %q", session.Options.Path, originalPath) 53 | } 54 | } 55 | 56 | // Test for GH-2. 57 | func TestGH2MaxLength(t *testing.T) { 58 | store := NewFilesystemStore("", []byte("some key")) 59 | req, err := http.NewRequest("GET", "http://www.example.com", nil) 60 | if err != nil { 61 | t.Fatal("failed to create request", err) 62 | } 63 | w := httptest.NewRecorder() 64 | 65 | session, err := store.New(req, "my session") 66 | if err != nil { 67 | t.Fatal("failed to create session", err) 68 | } 69 | 70 | session.Values["big"] = make([]byte, base64.StdEncoding.DecodedLen(4096*2)) 71 | err = session.Save(req, w) 72 | if err == nil { 73 | t.Fatal("expected an error, got nil") 74 | } 75 | 76 | store.MaxLength(4096 * 3) // A bit more than the value size to account for encoding overhead. 77 | err = session.Save(req, w) 78 | if err != nil { 79 | t.Fatal("failed to Save:", err) 80 | } 81 | } 82 | 83 | // Test delete filesystem store with max-age: -1 84 | func TestGH8FilesystemStoreDelete(t *testing.T) { 85 | store := NewFilesystemStore("", []byte("some key")) 86 | req, err := http.NewRequest("GET", "http://www.example.com", nil) 87 | if err != nil { 88 | t.Fatal("failed to create request", err) 89 | } 90 | w := httptest.NewRecorder() 91 | 92 | session, err := store.New(req, "hello") 93 | if err != nil { 94 | t.Fatal("failed to create session", err) 95 | } 96 | 97 | err = session.Save(req, w) 98 | if err != nil { 99 | t.Fatal("failed to save session", err) 100 | } 101 | 102 | session.Options.MaxAge = -1 103 | err = session.Save(req, w) 104 | if err != nil { 105 | t.Fatal("failed to delete session", err) 106 | } 107 | } 108 | 109 | // Test delete filesystem store with max-age: 0 110 | func TestGH8FilesystemStoreDelete2(t *testing.T) { 111 | store := NewFilesystemStore("", []byte("some key")) 112 | req, err := http.NewRequest("GET", "http://www.example.com", nil) 113 | if err != nil { 114 | t.Fatal("failed to create request", err) 115 | } 116 | w := httptest.NewRecorder() 117 | 118 | session, err := store.New(req, "hello") 119 | if err != nil { 120 | t.Fatal("failed to create session", err) 121 | } 122 | 123 | err = session.Save(req, w) 124 | if err != nil { 125 | t.Fatal("failed to save session", err) 126 | } 127 | 128 | session.Options.MaxAge = 0 129 | err = session.Save(req, w) 130 | if err != nil { 131 | t.Fatal("failed to delete session", err) 132 | } 133 | } 134 | --------------------------------------------------------------------------------