Skip to content

Commit

Permalink
fix cookie domain parsing to ensure all subdomains are issued
Browse files Browse the repository at this point in the history
  • Loading branch information
Southclaws committed Aug 16, 2024
1 parent 853487a commit 0c131ab
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 14 deletions.
38 changes: 36 additions & 2 deletions app/transports/http/middleware/session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ package session
import (
"context"
"net/http"
"net/url"
"time"

"github.com/rs/xid"
"golang.org/x/net/publicsuffix"

"github.com/Southclaws/fault"
"github.com/Southclaws/fault/fmsg"
"github.com/Southclaws/storyden/app/resources/account"
"github.com/Southclaws/storyden/app/services/authentication/session"
"github.com/Southclaws/storyden/internal/config"
Expand All @@ -33,12 +37,42 @@ type Jar struct {
secureCookieName string
}

func New(cfg config.Config, ss *securecookie.Session) *Jar {
func New(cfg config.Config, ss *securecookie.Session) (*Jar, error) {
domain, err := getDomain(cfg.PublicAPIAddress)
if err != nil {
return nil, fault.Wrap(err, fmsg.With("failed to parse domain from public API address"))
}

return &Jar{
domain: cfg.PublicAPIAddress.Hostname(),
domain: domain,
ss: ss,
secureCookieName: secureCookieName,
}, nil
}

func getDomain(address url.URL) (string, error) {
// We want to use the site's domain, not the API's subdomain to ensure that
// cookies can be used in both the frontend and for the API. This assumption
// is based on the idea that Storyden must be hosted on a single domain with
// the API and frontend on different subdomains. For example, if your site
// was "www.cats.com" and the API was "api.cats.com", then the domain config
// would be set up with `www.cats.com` as the `PUBLIC_WEB_ADDRESS` and then
// `api.cats.com` as the `PUBLIC_API_ADDRESS` and then this code would use
// the API address hostname to parse `cats.com` as the actual cookie domain.
// The reason for this is that it makes SSR frontends trivial to implement.

hostname := address.Hostname()

if hostname == "localhost" {
return hostname, nil
}

domain, err := publicsuffix.EffectiveTLDPlusOne(hostname)
if err != nil {
return "", fault.Wrap(err, fmsg.With("failed to parse domain from public API address"))
}

return domain, nil
}

func (j *Jar) createWithValue(value string, expire time.Time) *http.Cookie {
Expand Down
18 changes: 10 additions & 8 deletions home/pages/docs/operating/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,22 @@ This specifies the primary database Storyden will persist all its main data in.
The interface bind address. Usually you won't ever need to change this.

### `COOKIE_DOMAIN`
### `PUBLIC_API_ADDRESS`

> default: `localhost`
> default: `http://localhost:8000`
This is important as it affects the ability for users to authenticate. It's used for both cookies and WebAuthn.

The hostname part of this URL is used to set the `Domain` attribute on cookies. This is important for security and privacy reasons. If you're running Storyden on a subdomain, you should set this to the full domain including the subdomain.

Warning: Changing this will break WebAuthn/Passkey sessions. There's a planned workaround coming but currently it's not recommended you switch domains if you allow users to authenticate with WebAuthn/Passkey.

### `PUBLIC_WEB_ADDRESS`

> default: `http://localhost:3000`
The public address for the frontend application. Change this to your production frontend URL with which you access the web application from via a browser. In production it should not have a port number and should use https.

### `SESSION_KEY`

> default: `0000000000000000`
Expand All @@ -59,12 +67,6 @@ An encryption key for secure cookies. Do not leave this as the default value for
believe the key has been compromised.
</Callout>

### `PUBLIC_WEB_ADDRESS`

> default: `http://localhost:3000`
The public address for the frontend application. Change this to your production frontend URL with which you access the web application from via a browser. In production it should not have a port number and should use https.

## Optional

### `ASSET_STORAGE_TYPE`
Expand Down
20 changes: 16 additions & 4 deletions home/pages/docs/quickstart/fly.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ You can visit the URL that Fly.io gives you and you'll see your Storyden instanc
That's only half the story though. You have an instance but

1. it's entirely **ephemeral**! This means that if the instance is restarted, it will lose all its data.
2. it has no idea what its domain name is! This means session cookies won't work.
2. it has no idea what its public address is! This means session cookies won't work.

### Persistent storage

Expand Down Expand Up @@ -121,14 +121,14 @@ Here's an example of the environment variables section in the `fly.toml` file:

```toml
[env]
COOKIE_DOMAIN = "storyden-unique-name-1234.fly.dev"
PUBLIC_API_ADDRESS = "https://storyden-unique-name-1234.fly.dev"
PUBLIC_WEB_ADDRESS = "https://storyden-unique-name-1234.fly.dev"
SESSION_KEY = "<your key>"
```

#### `COOKIE_DOMAIN` and `PUBLIC_WEB_ADDRESS`
#### `PUBLIC_API_ADDRESS` and `PUBLIC_WEB_ADDRESS`

The [cookie domain](/docs/operating/configuration#cookie_domain) and [public web address](/docs/operating/configuration#public_web_address) variables are necessary for Storyden to manage sessions using cookies and a few other things such as knowing where links should point to.
The [public API address](/docs/operating/configuration#public_api_address) and [public web address](/docs/operating/configuration#public_web_address) variables are necessary for Storyden to manage sessions using cookies and a few other things such as knowing where links should point to.

In the above section, when you ran `fly launch`, it gives you the URL of your app. This URL will look something like:

Expand All @@ -138,6 +138,18 @@ https://storyden-unique-name-1234.fly.dev

It's a subdomain of `fly.dev` and is uniquely generated based on your app name. You can refer to the Fly.io domains documentation for information on setting up a custom domain.

<Callout type="info" emoji="ℹ️">
For this quick start guide, your instance is all hosted on a single domain,
that means the API and the frontend both run on
`https://storyden-unique-name-1234.fly.dev`. But if you're using separate
backend and frontend deployments (hosting the frontend on Vercel for example)
it's necessary that both share the same root domain. So you'd put the API on
something like `api.mycommunity.com` and the frontend on `mycommunity.com`.
Cookies will be issued to the root domain, `mycommunity.com` in this example
and will be included in requests to both the API and the frontend's SSR
rendering backend if you're using a framework such as Next.js.
</Callout>

#### `SESSION_KEY`

The [session key](/docs/operating/configuration#session_key) is an important security feature. It's used to encrypt session tokens so that they can't be tampered with. It's important to set this to a unique value that is kept secret. You can use any trusted source of randomness on your local machine to generate it.
Expand Down

0 comments on commit 0c131ab

Please sign in to comment.