Files
wonderwall/pkg/cookie/cookie.go
2023-02-10 14:57:53 +01:00

145 lines
3.1 KiB
Go

package cookie
import (
"encoding/base64"
"errors"
"fmt"
"net/http"
"time"
"github.com/nais/wonderwall/pkg/crypto"
)
const (
Session = "io.nais.wonderwall.session"
Login = "io.nais.wonderwall.callback"
LoginLegacy = "io.nais.wonderwall.callback.legacy"
Retry = "io.nais.wonderwall.retry"
)
var (
ErrInvalidValue = errors.New("invalid value")
)
type Cookie struct {
*http.Cookie
}
func (in *Cookie) Encrypt(crypter crypto.Crypter) (*Cookie, error) {
plaintext := []byte(in.Cookie.Value)
ciphertext, err := crypter.Encrypt(plaintext)
if err != nil {
return nil, fmt.Errorf("unable to encrypt cookie '%s': %w", in.Cookie.Name, err)
}
value := base64.StdEncoding.EncodeToString(ciphertext)
in.Cookie.Value = value
return in, nil
}
func (in *Cookie) Decrypt(crypter crypto.Crypter) (string, error) {
ciphertext, err := base64.StdEncoding.DecodeString(in.Value)
if err != nil {
return "", fmt.Errorf("%w: named '%s': %+v", ErrInvalidValue, in.Name, err)
}
plaintext, err := crypter.Decrypt(ciphertext)
if err != nil {
return "", fmt.Errorf("unable to decrypt cookie '%s': %w", in.Name, err)
}
return string(plaintext), err
}
// UnsetExpiry sets the MaxAge and Expires fields to their 'nil' values to unset them. For most user agents, this means
// that the cookie should expire at the 'end of a session', typically when the browser is closed.
//
// The cookie should still be explicitly cleared/expired whenever it is no longer needed.
func (in *Cookie) UnsetExpiry() {
in.MaxAge = 0
in.Expires = time.Time{}
}
func Clear(w http.ResponseWriter, name string, opts Options) {
expires := time.Unix(0, 0)
maxAge := -1
cookie := &http.Cookie{
Expires: expires,
HttpOnly: true,
MaxAge: maxAge,
Name: name,
Path: "/",
SameSite: opts.SameSite,
Secure: opts.Secure,
}
if len(opts.Domain) > 0 {
cookie.Domain = opts.Domain
}
if len(opts.Path) > 0 {
cookie.Path = opts.Path
}
http.SetCookie(w, cookie)
}
func Get(r *http.Request, key string) (*Cookie, error) {
cookie, err := r.Cookie(key)
if err != nil {
return nil, fmt.Errorf("no cookie named '%s': %w", key, err)
}
return &Cookie{cookie}, nil
}
func GetDecrypted(r *http.Request, key string, crypter crypto.Crypter) (string, error) {
encryptedCookie, err := Get(r, key)
if err != nil {
return "", err
}
return encryptedCookie.Decrypt(crypter)
}
func Make(name, value string, opts Options) *Cookie {
expires := time.Now().Add(opts.ExpiresIn)
maxAge := int(opts.ExpiresIn.Seconds())
cookie := &http.Cookie{
Expires: expires,
HttpOnly: true,
MaxAge: maxAge,
Name: name,
Path: "/",
SameSite: opts.SameSite,
Secure: opts.Secure,
Value: value,
}
if len(opts.Domain) > 0 {
cookie.Domain = opts.Domain
}
if len(opts.Path) > 0 {
cookie.Path = opts.Path
}
return &Cookie{cookie}
}
func Set(w http.ResponseWriter, cookie *Cookie) {
http.SetCookie(w, cookie.Cookie)
}
func EncryptAndSet(w http.ResponseWriter, key, value string, opts Options, crypter crypto.Crypter) error {
encryptedCookie, err := Make(key, value, opts).Encrypt(crypter)
if err != nil {
return err
}
Set(w, encryptedCookie)
return nil
}