github.com/greenpau/go-authcrunch@v1.0.50/pkg/authn/cookie/cookie.go (about) 1 // Copyright 2022 Paul Greenberg greenpau@outlook.com 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package cookie 16 17 import ( 18 "fmt" 19 "net" 20 "sort" 21 "strings" 22 ) 23 24 // Config represents a common set of configuration settings 25 // applicable to the cookies issued by authn.Authenticator. 26 type Config struct { 27 Domains map[string]*DomainConfig `json:"domains,omitempty" xml:"domains,omitempty" yaml:"domains,omitempty"` 28 Path string `json:"path,omitempty" xml:"path,omitempty" yaml:"path,omitempty"` 29 Lifetime int `json:"lifetime,omitempty" xml:"lifetime,omitempty" yaml:"lifetime,omitempty"` 30 Insecure bool `json:"insecure,omitempty" xml:"insecure,omitempty" yaml:"insecure,omitempty"` 31 SameSite string `json:"same_site,omitempty" xml:"same_site,omitempty" yaml:"same_site,omitempty"` 32 StripDomainEnabled bool `json:"strip_domain_enabled,omitempty" xml:"strip_domain_enabled,omitempty" yaml:"strip_domain_enabled,omitempty"` 33 } 34 35 // DomainConfig represents a common set of configuration settings 36 // applicable to the cookies issued by authn.Authenticator. 37 type DomainConfig struct { 38 Seq int `json:"seq,omitempty" xml:"seq,omitempty" yaml:"seq,omitempty"` 39 Domain string `json:"domain,omitempty" xml:"domain,omitempty" yaml:"domain,omitempty"` 40 Path string `json:"path,omitempty" xml:"path,omitempty" yaml:"path,omitempty"` 41 Lifetime int `json:"lifetime,omitempty" xml:"lifetime,omitempty" yaml:"lifetime,omitempty"` 42 Insecure bool `json:"insecure,omitempty" xml:"insecure,omitempty" yaml:"insecure,omitempty"` 43 SameSite string `json:"same_site,omitempty" xml:"same_site,omitempty" yaml:"same_site,omitempty"` 44 StripDomainEnabled bool `json:"strip_domain_enabled,omitempty" xml:"strip_domain_enabled,omitempty" yaml:"strip_domain_enabled,omitempty"` 45 } 46 47 // Factory holds configuration and associated finctions 48 // for the cookies issued by authn.Authenticator. 49 type Factory struct { 50 config *Config 51 domains []string 52 Referer string `json:"referer,omitempty" xml:"referer,omitempty" yaml:"referer,omitempty"` 53 SessionID string `json:"session_id,omitempty" xml:"session_id,omitempty" yaml:"session_id,omitempty"` 54 SandboxID string `json:"sandbox_id,omitempty" xml:"sandbox_id,omitempty" yaml:"sandbox_id,omitempty"` 55 } 56 57 // NewFactory returns an instance of cookie factory. 58 func NewFactory(c *Config) (*Factory, error) { 59 f := &Factory{} 60 if c == nil { 61 f.config = &Config{} 62 } else { 63 f.config = c 64 } 65 if f.config.Domains != nil { 66 domains := []string{} 67 domainList := []*DomainConfig{} 68 for _, v := range f.config.Domains { 69 domainList = append(domainList, v) 70 } 71 sort.SliceStable(domainList, func(i, j int) bool { 72 return domainList[i].Seq < domainList[j].Seq 73 }) 74 for _, v := range domainList { 75 domains = append(domains, v.Domain) 76 } 77 f.domains = domains 78 } 79 f.Referer = "AUTHP_REDIRECT_URL" 80 f.SessionID = "AUTHP_SESSION_ID" 81 f.SandboxID = "AUTHP_SANDBOX_ID" 82 switch strings.ToLower(f.config.SameSite) { 83 case "": 84 case "lax", "strict", "none": 85 f.config.SameSite = strings.Title(f.config.SameSite) 86 default: 87 return nil, fmt.Errorf("the SameSite cookie attribute %q is invalid", f.config.SameSite) 88 } 89 90 return f, nil 91 } 92 93 // GetCookie returns raw cookie string from key-value input. 94 func (f *Factory) GetCookie(h, k, v string) string { 95 var sb strings.Builder 96 sb.WriteString(k + "=" + v + ";") 97 98 entry := f.evalHost(h) 99 if entry != nil && entry.Domain != "" { 100 sb.WriteString(fmt.Sprintf(" Domain=%s;", entry.Domain)) 101 } 102 103 switch { 104 case entry != nil && entry.Path != "": 105 sb.WriteString(fmt.Sprintf(" Path=%s;", entry.Path)) 106 case f.config.Path != "": 107 sb.WriteString(fmt.Sprintf(" Path=%s;", f.config.Path)) 108 default: 109 sb.WriteString(" Path=/;") 110 } 111 112 switch { 113 case entry != nil && entry.Lifetime != 0: 114 sb.WriteString(fmt.Sprintf(" Max-Age=%d;", entry.Lifetime)) 115 case f.config.Lifetime != 0: 116 sb.WriteString(fmt.Sprintf(" Max-Age=%d;", f.config.Lifetime)) 117 } 118 119 switch { 120 case entry != nil && entry.SameSite != "": 121 sb.WriteString(fmt.Sprintf(" SameSite=%s;", entry.SameSite)) 122 case f.config.SameSite != "": 123 sb.WriteString(fmt.Sprintf(" SameSite=%s;", f.config.SameSite)) 124 } 125 126 switch { 127 case entry != nil && !entry.Insecure: 128 sb.WriteString(" Secure; HttpOnly;") 129 case !f.config.Insecure: 130 sb.WriteString(" Secure; HttpOnly;") 131 } 132 133 return sb.String() 134 } 135 136 // GetIdentityTokenCookie returns raw identity token cookie string from key-value input. 137 func (f *Factory) GetIdentityTokenCookie(k, v string) string { 138 var sb strings.Builder 139 sb.WriteString(k + "=" + v + ";") 140 sb.WriteString(" Path=/;") 141 if f.config.Lifetime != 0 { 142 sb.WriteString(fmt.Sprintf(" Max-Age=%d;", f.config.Lifetime)) 143 } 144 if f.config.SameSite != "" { 145 sb.WriteString(fmt.Sprintf(" SameSite=%s;", f.config.SameSite)) 146 } 147 if !f.config.Insecure { 148 sb.WriteString(" Secure; HttpOnly;") 149 } 150 return sb.String() 151 } 152 153 // GetSessionCookie return cookie holding session information 154 func (f *Factory) GetSessionCookie(h, s string) string { 155 var sb strings.Builder 156 sb.WriteString(fmt.Sprintf("%s=%s;", f.SessionID, s)) 157 entry := f.evalHost(h) 158 if entry != nil && entry.Domain != "" { 159 sb.WriteString(fmt.Sprintf(" Domain=%s;", entry.Domain)) 160 } 161 162 sb.WriteString(" Path=/;") 163 164 switch { 165 case entry != nil && !entry.Insecure: 166 sb.WriteString(" Secure; HttpOnly;") 167 case !f.config.Insecure: 168 sb.WriteString(" Secure; HttpOnly;") 169 } 170 171 return sb.String() 172 } 173 174 // GetDeleteCookie returns raw cookie with attributes for delete action. 175 func (f *Factory) GetDeleteCookie(h, s string) string { 176 var sb strings.Builder 177 sb.WriteString(s) 178 sb.WriteString("=delete;") 179 entry := f.evalHost(h) 180 if entry != nil && entry.Domain != "" { 181 sb.WriteString(fmt.Sprintf(" Domain=%s;", entry.Domain)) 182 } 183 184 switch { 185 case entry != nil && entry.Path != "": 186 sb.WriteString(fmt.Sprintf(" Path=%s;", entry.Path)) 187 case f.config.Path != "": 188 sb.WriteString(fmt.Sprintf(" Path=%s;", f.config.Path)) 189 default: 190 sb.WriteString(" Path=/;") 191 } 192 193 sb.WriteString(" Expires=Thu, 01 Jan 1970 00:00:00 GMT;") 194 return sb.String() 195 } 196 197 // GetDeleteSessionCookie returns raw cookie with attributes for delete action 198 // for session id cookie. 199 func (f *Factory) GetDeleteSessionCookie(h string) string { 200 var sb strings.Builder 201 sb.WriteString(f.SessionID) 202 sb.WriteString("=delete;") 203 entry := f.evalHost(h) 204 if entry != nil && entry.Domain != "" { 205 sb.WriteString(fmt.Sprintf(" Domain=%s;", entry.Domain)) 206 } 207 sb.WriteString(" Path=/;") 208 sb.WriteString(" Expires=Thu, 01 Jan 1970 00:00:00 GMT;") 209 return sb.String() 210 } 211 212 // GetDeleteIdentityTokenCookie returns raw identity token cookie with attributes for delete action. 213 func (f *Factory) GetDeleteIdentityTokenCookie(s string) string { 214 var sb strings.Builder 215 sb.WriteString(s) 216 sb.WriteString("=delete;") 217 sb.WriteString(" Path=/;") 218 sb.WriteString(" Expires=Thu, 01 Jan 1970 00:00:00 GMT;") 219 return sb.String() 220 } 221 222 func (f *Factory) evalHost(h string) *DomainConfig { 223 i := strings.IndexByte(h, ':') 224 if i > 0 { 225 if strings.Count(h, ":") > 1 { 226 // IPv6 address found. 227 return nil 228 } 229 // There is a host:port separator. 230 h = h[:i] 231 } 232 if addr := net.ParseIP(h); addr != nil { 233 // This is IP address. 234 return nil 235 } 236 237 if strings.Count(h, ".") == 0 { 238 // This is hostname without domain. 239 return nil 240 } 241 242 if len(f.domains) > 0 { 243 var candidate *DomainConfig 244 for _, k := range f.domains { 245 if h == k { 246 return f.config.Domains[k] 247 } 248 if strings.Contains(h, k) { 249 candidate = f.config.Domains[k] 250 } 251 } 252 if candidate != nil { 253 // Partial match between the provided hostname and the config domain. 254 return candidate 255 } 256 } 257 258 c := &DomainConfig{} 259 260 if strings.Count(h, ".") == 1 { 261 c.Domain = string(h) 262 } else { 263 i = strings.IndexByte(h, '.') 264 c.Domain = string(h[i+1:]) 265 } 266 267 if f.config.StripDomainEnabled { 268 c.Domain = "" 269 } 270 271 c.Path = f.config.Path 272 c.Lifetime = f.config.Lifetime 273 c.Insecure = f.config.Insecure 274 c.SameSite = f.config.SameSite 275 return c 276 }