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  }