Skip to content

Commit 526903c

Browse files
Update using custom domain minus one as cookie domain
ref #1393
2 parents 115c7ba + deecf1d commit 526903c

22 files changed

+268
-104
lines changed

cmd/portal/cobraviper.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,8 @@ var ArgPlanNameForAppUpdate = &cobraviper.StringArgument{
6464
Usage: "Plan name",
6565
DefaultValue: "custom",
6666
}
67+
68+
var ArgAppHostSuffix = &cobraviper.StringArgument{
69+
ArgumentName: "app-host-suffix",
70+
Usage: "App host suffix",
71+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package main
2+
3+
import (
4+
"encoding/base64"
5+
"fmt"
6+
"log"
7+
"net/url"
8+
"strings"
9+
10+
"github.com/spf13/cobra"
11+
"sigs.k8s.io/yaml"
12+
13+
"github.com/authgear/authgear-server/cmd/portal/internal"
14+
"github.com/authgear/authgear-server/pkg/util/httputil"
15+
)
16+
17+
var migrateCookieDomainAppHostSuffix string
18+
19+
var cmdInternalMigrateCookieDomain = &cobra.Command{
20+
Use: "migrate-cookie-domain",
21+
Short: "Set cookie domain for apps which are using custom domain",
22+
RunE: func(cmd *cobra.Command, args []string) error {
23+
binder := getBinder()
24+
25+
dbURL, err := binder.GetRequiredString(cmd, ArgDatabaseURL)
26+
if err != nil {
27+
return err
28+
}
29+
30+
dbSchema, err := binder.GetRequiredString(cmd, ArgDatabaseSchema)
31+
if err != nil {
32+
return err
33+
}
34+
35+
migrateCookieDomainAppHostSuffix, err = binder.GetRequiredString(cmd, ArgAppHostSuffix)
36+
if err != nil {
37+
return err
38+
}
39+
40+
internal.MigrateResources(&internal.MigrateResourcesOptions{
41+
DatabaseURL: dbURL,
42+
DatabaseSchema: dbSchema,
43+
UpdateConfigSourceFunc: migrateCookieDomain,
44+
DryRun: &MigrateResourcesDryRun,
45+
})
46+
47+
return nil
48+
},
49+
}
50+
51+
func migrateCookieDomain(appID string, configSourceData map[string]string, dryRun bool) error {
52+
encodedData := configSourceData["authgear.yaml"]
53+
decoded, err := base64.StdEncoding.DecodeString(encodedData)
54+
if err != nil {
55+
return fmt.Errorf("failed decode authgear.yaml: %w", err)
56+
}
57+
58+
if dryRun {
59+
log.Printf("Converting app (%s)", appID)
60+
log.Printf("Before updated:")
61+
log.Printf("\n%s\n", string(decoded))
62+
}
63+
64+
m := make(map[string]interface{})
65+
err = yaml.Unmarshal(decoded, &m)
66+
if err != nil {
67+
return fmt.Errorf("failed unmarshal yaml: %w", err)
68+
}
69+
70+
httpConfig, ok := m["http"].(map[string]interface{})
71+
if !ok {
72+
return nil
73+
}
74+
75+
publicOrigin, ok := httpConfig["public_origin"].(string)
76+
if !ok {
77+
return fmt.Errorf("cannot read public origin from authgear.yaml: %s", appID)
78+
}
79+
80+
if strings.HasSuffix(publicOrigin, migrateCookieDomainAppHostSuffix) {
81+
// skip default domain
82+
log.Printf("skip default domain...")
83+
return nil
84+
}
85+
86+
_, ok = httpConfig["cookie_domain"].(string)
87+
if ok {
88+
// skip the config that has cookie_domain
89+
log.Printf("skip config that has cookie_domain...")
90+
return nil
91+
}
92+
93+
u, err := url.Parse(publicOrigin)
94+
if err != nil {
95+
return fmt.Errorf("failed to parse public origin: %w", err)
96+
}
97+
98+
cookieDomain := httputil.CookieDomainWithoutPort(u.Host)
99+
httpConfig["cookie_domain"] = cookieDomain
100+
101+
migrated, err := yaml.Marshal(m)
102+
if err != nil {
103+
return fmt.Errorf("failed marshal yaml: %w", err)
104+
}
105+
106+
if dryRun {
107+
log.Printf("After updated:")
108+
log.Printf("\n%s\n", string(migrated))
109+
}
110+
111+
configSourceData["authgear.yaml"] = base64.StdEncoding.EncodeToString(migrated)
112+
return nil
113+
}
114+
115+
func init() {
116+
binder := getBinder()
117+
cmdInternalBreakingChangeMigrateResources.AddCommand(cmdInternalMigrateCookieDomain)
118+
binder.BindString(cmdInternalMigrateCookieDomain.Flags(), ArgAppHostSuffix)
119+
}

pkg/portal/graphql/domain.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ var domain = graphql.NewObject(graphql.ObjectConfig{
1111
"id": &graphql.Field{Type: graphql.NewNonNull(graphql.String)},
1212
"createdAt": &graphql.Field{Type: graphql.NewNonNull(graphql.DateTime)},
1313
"domain": &graphql.Field{Type: graphql.NewNonNull(graphql.String)},
14+
"cookieDomain": &graphql.Field{Type: graphql.NewNonNull(graphql.String)},
1415
"apexDomain": &graphql.Field{Type: graphql.NewNonNull(graphql.String)},
1516
"verificationDNSRecord": &graphql.Field{Type: graphql.NewNonNull(graphql.String)},
1617
"isCustom": &graphql.Field{Type: graphql.NewNonNull(graphql.Boolean)},

pkg/portal/model/domain.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type Domain struct {
1313
AppID string `json:"appID"`
1414
CreatedAt time.Time `json:"createdAt"`
1515
Domain string `json:"domain"`
16+
CookieDomain string `json:"cookieDomain"`
1617
ApexDomain string `json:"apexDomain"`
1718
VerificationDNSRecord string `json:"verificationDNSRecord"`
1819
IsCustom bool `json:"isCustom"`

pkg/portal/service/domain.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/authgear/authgear-server/pkg/lib/infra/db/globaldb"
2121
"github.com/authgear/authgear-server/pkg/portal/model"
2222
"github.com/authgear/authgear-server/pkg/util/clock"
23+
"github.com/authgear/authgear-server/pkg/util/httputil"
2324
corerand "github.com/authgear/authgear-server/pkg/util/rand"
2425
"github.com/authgear/authgear-server/pkg/util/uuid"
2526
)
@@ -408,12 +409,21 @@ func (d *domain) toModel(isVerified bool) *model.Domain {
408409
prefix = "pending:"
409410
}
410411

412+
// for default domain, original domain will be used for cookie domain
413+
// for custom domain, cookie domain is derived from the
414+
// CookieDomainWithoutPort function
415+
cookieDomain := d.Domain
416+
if d.IsCustom {
417+
cookieDomain = httputil.CookieDomainWithoutPort(d.Domain)
418+
}
419+
411420
return &model.Domain{
412421
// Base64-encoded to avoid invalid k8s resource label invalid chars
413422
ID: base64.RawURLEncoding.EncodeToString([]byte(prefix + d.ID)),
414423
AppID: d.AppID,
415424
CreatedAt: d.CreatedAt,
416425
Domain: d.Domain,
426+
CookieDomain: cookieDomain,
417427
ApexDomain: d.ApexDomain,
418428
VerificationDNSRecord: domainVerificationDNSRecord(d.VerificationNonce),
419429
IsCustom: d.IsCustom,

pkg/util/httputil/cookie.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,12 @@ func UpdateCookie(w http.ResponseWriter, cookie *http.Cookie) {
4141
header["Set-Cookie"] = setCookies
4242
}
4343

44-
// CookieDomainFromETLDPlusOneWithoutPort derives host from r.
44+
// CookieDomainWithoutPort derives host from r.
4545
// If host has port, the port is removed.
46+
// If host-1 is longer than ETLD+1, host-1 is returned.
4647
// If ETLD+1 cannot be derived, an empty string is returned.
4748
// The return value never have port.
48-
func CookieDomainFromETLDPlusOneWithoutPort(host string) string {
49+
func CookieDomainWithoutPort(host string) string {
4950
// Trim the port if it is present.
5051
// We have to trim the port first.
5152
// Passing host:port to EffectiveTLDPlusOne confuses it.
@@ -64,12 +65,18 @@ func CookieDomainFromETLDPlusOneWithoutPort(host string) string {
6465
return ""
6566
}
6667

67-
host, err := publicsuffix.EffectiveTLDPlusOne(host)
68+
eTLDPlusOne, err := publicsuffix.EffectiveTLDPlusOne(host)
6869
if err != nil {
6970
return ""
7071
}
7172

72-
return host
73+
// host has a valid ETLDPlusOne, it's safe to split the domain with .
74+
hostMinusOne := host[1+strings.Index(host, "."):]
75+
if len(hostMinusOne) > len(eTLDPlusOne) {
76+
return hostMinusOne
77+
}
78+
79+
return eTLDPlusOne
7380
}
7481

7582
type CookieManager struct {
@@ -85,7 +92,7 @@ func (f *CookieManager) fixupCookie(cookie *http.Cookie) {
8592

8693
cookie.Secure = proto == "https"
8794
if cookie.Domain == "" {
88-
cookie.Domain = CookieDomainFromETLDPlusOneWithoutPort(host)
95+
cookie.Domain = CookieDomainWithoutPort(host)
8996
}
9097

9198
if cookie.SameSite == http.SameSiteNoneMode &&

pkg/util/httputil/cookie_test.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,10 @@ func TestUpdateCookie(t *testing.T) {
106106
})
107107
}
108108

109-
func TestCookieDomainFromETLDPlusOneWithoutPort(t *testing.T) {
110-
Convey("CookieDomainFromETLDPlusOneWithoutPort", t, func() {
109+
func TestCookieDomainWithoutPort(t *testing.T) {
110+
Convey("CookieDomainWithoutPort", t, func() {
111111
check := func(in string, out string) {
112-
actual := httputil.CookieDomainFromETLDPlusOneWithoutPort(in)
112+
actual := httputil.CookieDomainWithoutPort(in)
113113
So(out, ShouldEqual, actual)
114114
}
115115
check("localhost", "")
@@ -139,5 +139,9 @@ func TestCookieDomainFromETLDPlusOneWithoutPort(t *testing.T) {
139139
check("www.example.co.jp", "example.co.jp")
140140
check("www.example.co.jp:80", "example.co.jp")
141141
check("www.example.co.jp:8080", "example.co.jp")
142+
143+
check("auth.app.example.co.jp", "app.example.co.jp")
144+
check("auth.app.example.co.jp:80", "app.example.co.jp")
145+
check("auth.app.example.co.jp:8080", "app.example.co.jp")
142146
})
143147
}

portal/src/graphql/portal/ApplicationsConfigurationScreen.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const COPY_ICON_STLYES: IButtonStyles = {
4747

4848
interface FormState {
4949
publicOrigin: string;
50+
cookieDomain?: string;
5051
clients: OAuthClientConfig[];
5152
allowedOrigins: string[];
5253
persistentCookie: boolean;
@@ -58,6 +59,7 @@ interface FormState {
5859
function constructFormState(config: PortalAPIAppConfig): FormState {
5960
return {
6061
publicOrigin: config.http?.public_origin ?? "",
62+
cookieDomain: config.http?.cookie_domain,
6163
clients: config.oauth?.clients ?? [],
6264
allowedOrigins: config.http?.allowed_origins ?? [],
6365
persistentCookie: !(config.session?.cookie_non_persistent ?? false),
@@ -284,7 +286,8 @@ const SessionConfigurationWidget: React.FC<SessionConfigurationWidgetProps> =
284286
<FormattedMessage
285287
id="SessionConfigurationWidget.description"
286288
values={{
287-
endpoint: state.publicOrigin,
289+
// cookieDomain wil be empty only if authgear.yaml is updated manually
290+
domain: state.cookieDomain ?? state.publicOrigin,
288291
}}
289292
/>
290293
</WidgetDescription>

portal/src/graphql/portal/CustomDomainListScreen.module.scss

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
.content {
2-
margin: 0 32px;
1+
.root {
2+
display: flex;
3+
flex-direction: column;
34
}
45

56
.verifySuccessMessageBar {
@@ -10,12 +11,14 @@
1011
}
1112
}
1213

14+
.widget {
15+
margin: 10px 15px;
16+
max-width: 750px;
17+
}
18+
1319
.description {
1420
display: block;
1521
margin: 16px 8px;
16-
width: 650px;
17-
font-size: 13px;
18-
line-height: 1.3;
1922
}
2023

2124
.domainListColumn {

0 commit comments

Comments
 (0)