Repository navigation
Expand file tree
/
Copy pathoptions.go
More file actions
193 lines (177 loc) · 6.75 KB
/
Copy pathoptions.go
File metadata and controls
193 lines (177 loc) · 6.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
package seamlessauth
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"net/url"
"strings"
"time"
)
// Options configures an Adapter.
type Options struct {
// AuthServerURL is where the adapter reaches the auth API, without a trailing
// slash.
AuthServerURL string
// AuthServerIssuer is the expected iss of the auth API's tokens. Defaults to
// AuthServerURL. Set it when this server reaches the API at another URL than
// the issuer it advertises.
AuthServerIssuer string
// Audience is the expected aud of the auth API's tokens. The auth API sets it
// to its issuer, so it defaults to AuthServerIssuer.
Audience string
// CookieSecret signs the session cookies. At least 32 characters.
CookieSecret string
// ServiceSecret signs the service tokens the auth API trusts this adapter by.
// It holds the same value as the API's API_SERVICE_TOKEN; apps conventionally
// read it from SEAMLESS_SERVICE_TOKEN. At least 32 characters.
ServiceSecret string
// JWKSKid is the kid header on service tokens.
JWKSKid string
CookieDomain string
// InsecureCookies drops the Secure attribute, for local development over HTTP.
// It also lets the placeholder secrets from the examples through.
InsecureCookies bool
// CookieSameSite defaults to None for secure cookies and Lax otherwise.
CookieSameSite http.SameSite
// AllowedOrigins, when set, is the only origins allowed to make
// state-changing requests, whatever CookieSameSite is. A request marked
// Sec-Fetch-Site: same-site, or one without Sec-Fetch-Site that carries an
// Origin, must then carry an Origin on the list. A non-nil empty slice is set
// and allows no origin; leave it nil to not configure one.
AllowedOrigins []string
AccessCookieName string
RegistrationCookieName string
RefreshCookieName string
PreAuthCookieName string
// Deliver sends OTP codes and magic links through the application's own
// transports. When set, the adapter asks the auth API for the message instead
// of having the API send it.
Deliver func(ctx context.Context, delivery Delivery) error
// DeliverAccountNotices says Deliver also sends account_notice_email and
// account_notice_sms. Only then does the adapter ask the auth API to hand it
// the notices that tell an owner their account is being recovered; otherwise
// the API sends them itself. The API counts a notice it hands back as the
// owner being told, so set it only when Deliver really sends both kinds.
DeliverAccountNotices bool
// AllowRecoverySessions lets RequireAuth and Authenticate admit an access
// token carrying recovery: true, which a recovery code issues before the user
// has enrolled a new passkey or authenticator app. By default such a session
// is refused with 403 enrollment_required.
AllowRecoverySessions bool
// ResolveClientIP returns the end user's address. Defaults to the connecting
// peer. Behind a proxy, use TrustedProxies.
ResolveClientIP func(r *http.Request) string
// DisableManifestFetch uses only the manifest bundled with this version
// instead of fetching the auth API's on the first request.
DisableManifestFetch bool
// HTTPClient makes the calls to the auth API. It never follows a redirect: a
// supplied client is copied with a CheckRedirect that refuses them, and one
// with no Timeout gets the default.
HTTPClient *http.Client
Logf func(format string, args ...any)
}
const (
minSecretLength = 32
upstreamTimeout = 15 * time.Second
)
// isPlaceholderSecret is a value from the examples and starters, which length
// alone does not catch.
func isPlaceholderSecret(secret string) bool {
return strings.EqualFold(secret, "dev-only-cookie-signing-key-replace-me") ||
strings.EqualFold(secret, "GRAB_FROM_SEAMLESS_AUTH_PORTAL_DEV") ||
strings.Contains(strings.ToLower(secret), "replace-me")
}
func noRedirects(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
}
func (o Options) withDefaults() (Options, error) {
o.AuthServerURL = strings.TrimRight(strings.TrimSpace(o.AuthServerURL), "/")
if o.AuthServerURL == "" {
return o, errors.New("seamlessauth: AuthServerURL is required")
}
if u, err := url.Parse(o.AuthServerURL); err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
return o, errors.New("seamlessauth: AuthServerURL must be an http or https URL")
}
if len(o.CookieSecret) < minSecretLength {
return o, fmt.Errorf("seamlessauth: CookieSecret must be at least %d characters", minSecretLength)
}
if len(o.ServiceSecret) < minSecretLength {
return o, fmt.Errorf("seamlessauth: ServiceSecret must be at least %d characters", minSecretLength)
}
if o.Logf == nil {
o.Logf = log.Printf
}
for _, secret := range []struct{ name, value string }{
{"CookieSecret", o.CookieSecret},
{"ServiceSecret", o.ServiceSecret},
} {
if !isPlaceholderSecret(secret.value) {
continue
}
name := secret.name
if !o.InsecureCookies {
return o, fmt.Errorf("seamlessauth: %s is a placeholder value; set a real secret", name)
}
o.Logf("[seamless-auth] %s is a placeholder value, allowed only because InsecureCookies is set.", name)
}
if o.AuthServerIssuer == "" {
o.AuthServerIssuer = o.AuthServerURL
}
if o.Audience == "" {
o.Audience = o.AuthServerIssuer
}
if o.JWKSKid == "" {
o.JWKSKid = "dev-main"
o.Logf("[seamless-auth] JWKSKid is not set and defaults to %q, a placeholder. Set it to name the service-token key explicitly.", o.JWKSKid)
}
if o.CookieSameSite == 0 || o.CookieSameSite == http.SameSiteDefaultMode {
if o.InsecureCookies {
o.CookieSameSite = http.SameSiteLaxMode
} else {
o.CookieSameSite = http.SameSiteNoneMode
}
}
if o.AccessCookieName == "" {
o.AccessCookieName = "seamless-access"
}
if o.RegistrationCookieName == "" {
o.RegistrationCookieName = "seamless-ephemeral"
}
if o.RefreshCookieName == "" {
o.RefreshCookieName = "seamless-refresh"
}
// Shares the registration cookie on purpose: registration and sign-in never
// hold an ephemeral cookie at the same time.
if o.PreAuthCookieName == "" {
o.PreAuthCookieName = "seamless-ephemeral"
}
// A client that follows redirects would carry the service token and the
// user's bearer token to wherever the auth API pointed it.
if o.HTTPClient == nil {
o.HTTPClient = &http.Client{Timeout: upstreamTimeout}
} else {
client := *o.HTTPClient
o.HTTPClient = &client
}
o.HTTPClient.CheckRedirect = noRedirects
// A call that never returns would hold a refresh flight, and every tab
// waiting on it.
if o.HTTPClient.Timeout == 0 {
o.HTTPClient.Timeout = upstreamTimeout
}
return o, nil
}
func (o Options) cookieName(held string) string {
switch held {
case "access":
return o.AccessCookieName
case "registration":
return o.RegistrationCookieName
case "refresh":
return o.RefreshCookieName
default:
return o.PreAuthCookieName
}
}