/
grantcookie.go
139 lines (121 loc) · 3.67 KB
/
grantcookie.go
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
package auth
import (
"encoding/base64"
"encoding/json"
"net"
"net/http"
"strings"
"time"
"github.com/zalando/skipper/secrets"
"golang.org/x/oauth2"
)
type cookie struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
Expiry time.Time `json:"expiry,omitempty"`
Domain string `json:"domain,omitempty"`
}
func decodeCookie(cookieHeader string, config *OAuthConfig) (c *cookie, err error) {
var eb []byte
if eb, err = base64.StdEncoding.DecodeString(cookieHeader); err != nil {
return
}
var encryption secrets.Encryption
if encryption, err = config.Secrets.GetEncrypter(secretsRefreshInternal, config.SecretFile); err != nil {
return
}
var b []byte
if b, err = encryption.Decrypt(eb); err != nil {
return
}
err = json.Unmarshal(b, &c)
return
}
func (c *cookie) isAccessTokenExpired() bool {
now := time.Now()
return now.After(c.Expiry)
}
// allowedForHost checks if provided host matches cookie domain
// according to https://www.rfc-editor.org/rfc/rfc6265#section-5.1.3
func (c *cookie) allowedForHost(host string) bool {
hostWithoutPort, _, err := net.SplitHostPort(host)
if err != nil {
hostWithoutPort = host
}
return strings.HasSuffix(hostWithoutPort, c.Domain)
}
// extractCookie removes and returns the OAuth Grant token cookie from a HTTP request.
// The function supports multiple cookies with the same name and returns
// the best match (the one that decodes properly).
// The client may send multiple cookies if a parent domain has set a
// cookie of the same name.
// The grant token cookie is extracted so it does not get exposed to untrusted downstream
// services.
func extractCookie(request *http.Request, config *OAuthConfig) (*cookie, error) {
cookies := request.Cookies()
for i, c := range cookies {
if c.Name != config.TokenCookieName {
continue
}
decoded, err := decodeCookie(c.Value, config)
if err == nil && decoded.allowedForHost(request.Host) {
request.Header.Del("Cookie")
for j, c := range cookies {
if j != i {
request.AddCookie(c)
}
}
return decoded, nil
}
}
return nil, http.ErrNoCookie
}
// createDeleteCookie creates a cookie, which instructs the client to clear the grant
// token cookie when used with a Set-Cookie header.
func createDeleteCookie(config *OAuthConfig, host string) *http.Cookie {
return &http.Cookie{
Name: config.TokenCookieName,
Value: "",
Path: "/",
Domain: extractDomainFromHost(host, *config.TokenCookieRemoveSubdomains),
MaxAge: -1,
Secure: !config.Insecure,
HttpOnly: true,
}
}
func createCookie(config *OAuthConfig, host string, t *oauth2.Token) (*http.Cookie, error) {
domain := extractDomainFromHost(host, *config.TokenCookieRemoveSubdomains)
c := &cookie{
AccessToken: t.AccessToken,
RefreshToken: t.RefreshToken,
Expiry: t.Expiry,
Domain: domain,
}
b, err := json.Marshal(c)
if err != nil {
return nil, err
}
encryption, err := config.Secrets.GetEncrypter(secretsRefreshInternal, config.SecretFile)
if err != nil {
return nil, err
}
eb, err := encryption.Encrypt(b)
if err != nil {
return nil, err
}
b64 := base64.StdEncoding.EncodeToString(eb)
// The cookie expiry date must not be the same as the access token
// expiry. Otherwise the browser deletes the cookie as soon as the
// access token expires, but _before_ the refresh token has expired.
// Since we don't know the actual refresh token expiry, set it to
// 30 days as a good compromise.
return &http.Cookie{
Name: config.TokenCookieName,
Value: b64,
Path: "/",
Domain: domain,
Expires: t.Expiry.Add(time.Hour * 24 * 30),
Secure: !config.Insecure,
HttpOnly: true,
}, nil
}