Skip to content

Commit

Permalink
add initial cookie controller
Browse files Browse the repository at this point in the history
  • Loading branch information
equinox0815 committed Nov 5, 2023
1 parent faa43b0 commit 424ca36
Show file tree
Hide file tree
Showing 7 changed files with 384 additions and 47 deletions.
4 changes: 3 additions & 1 deletion cmd/whawty-nginx-sso/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"os"

"github.com/spreadspace/tlsconfig"
"github.com/whawty/nginx-sso/cookie"
"gopkg.in/yaml.v3"
)

Expand All @@ -43,7 +44,8 @@ type WebConfig struct {
}

type Config struct {
Web WebConfig `yaml:"web"`
Web WebConfig `yaml:"web"`
Cookie cookie.Config `yaml:"cookie"`
}

func readConfig(configfile string) (*Config, error) {
Expand Down
8 changes: 7 additions & 1 deletion cmd/whawty-nginx-sso/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"sync"

"github.com/urfave/cli"
"github.com/whawty/nginx-sso/cookie"
)

var (
Expand All @@ -57,14 +58,19 @@ func cmdRun(c *cli.Context) error {
return cli.NewExitError(err.Error(), 1)
}

cookies, err := cookie.NewController(&conf.Cookie)
if err != nil {
return cli.NewExitError(err.Error(), 2)
}

webAddrs := c.StringSlice("web-addr")
var wg sync.WaitGroup
for _, webAddr := range webAddrs {
a := webAddr
wg.Add(1)
go func() {
defer wg.Done()
if err := runWebAddr(a, &conf.Web); err != nil {
if err := runWebAddr(a, &conf.Web, cookies); err != nil {
fmt.Printf("warning running web interface(%s) failed: %s\n", a, err)
}
}()
Expand Down
11 changes: 6 additions & 5 deletions cmd/whawty-nginx-sso/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (

"github.com/flosch/pongo2/v6"
"github.com/gin-gonic/gin"
"github.com/whawty/nginx-sso/cookie"
"github.com/whawty/nginx-sso/ui"
"gitlab.com/go-box/pongo2gin/v6"
)
Expand Down Expand Up @@ -78,7 +79,7 @@ func webHandleLogout(c *gin.Context) {
c.Status(http.StatusNotImplemented)
}

func runWeb(listener net.Listener, config *WebConfig) (err error) {
func runWeb(listener net.Listener, config *WebConfig, cookies *cookie.Controller) (err error) {
gin.SetMode(gin.ReleaseMode)

r := gin.New()
Expand Down Expand Up @@ -109,17 +110,17 @@ func runWeb(listener net.Listener, config *WebConfig) (err error) {
return server.Serve(listener)
}

func runWebAddr(addr string, config *WebConfig) (err error) {
func runWebAddr(addr string, config *WebConfig, cookies *cookie.Controller) (err error) {
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return runWeb(ln.(*net.TCPListener), config)
return runWeb(ln.(*net.TCPListener), config, cookies)
}

func runWebListener(listener *net.TCPListener, config *WebConfig) (err error) {
return runWeb(listener, config)
func runWebListener(listener *net.TCPListener, config *WebConfig, cookies *cookie.Controller) (err error) {
return runWeb(listener, config, cookies)
}
98 changes: 58 additions & 40 deletions contrib/sample-cfg.yml
Original file line number Diff line number Diff line change
@@ -1,40 +1,58 @@
web:
# tls:
# certificate: "/path/to/server-crt.pem"
# certificate-key: "/path/to/server-key.pem"
# min-protocol-version: "TLSv1.2"
# # max-protocol-version: "TLSv1.3"
# ciphers:
# # - RSA_WITH_RC4_128_SHA
# # - RSA_WITH_3DES_EDE_CBC_SHA
# # - RSA_WITH_AES_128_CBC_SHA
# # - RSA_WITH_AES_256_CBC_SHA
# # - RSA_WITH_AES_128_CBC_SHA256
# # - RSA_WITH_AES_128_GCM_SHA256
# # - RSA_WITH_AES_256_GCM_SHA384
# # - ECDHE_ECDSA_WITH_RC4_128_SHA
# # - ECDHE_ECDSA_WITH_AES_128_CBC_SHA
# # - ECDHE_ECDSA_WITH_AES_256_CBC_SHA
# # - ECDHE_RSA_WITH_RC4_128_SHA
# # - ECDHE_RSA_WITH_3DES_EDE_CBC_SHA
# # - ECDHE_RSA_WITH_AES_128_CBC_SHA
# # - ECDHE_RSA_WITH_AES_256_CBC_SHA
# # - ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
# # - ECDHE_RSA_WITH_AES_128_CBC_SHA256
# - ECDHE_RSA_WITH_AES_128_GCM_SHA256
# # - ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
# - ECDHE_RSA_WITH_AES_256_GCM_SHA384
# # - ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
# - ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
# # - ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
# - TLS_AES_128_GCM_SHA256
# - TLS_AES_256_GCM_SHA384
# - TLS_CHACHA20_POLY1305_SHA256
# prefer-server-ciphers: true
# # ecdh-curves:
# # - secp256r1
# # - secp384r1
# # - secp521r1
# # - x25519
# # session-tickets: true
# # session-ticket-key: "b947e39f50e20351bdd81046e20fff7948d359a3aec391719d60645c5972cc77"
cookie:
domain: example.com
name: __Secure-whawty-nignx-sso
secure: true
expire: 23h
signers:
- name: foo
ed25519:
## generate with `openssl genpkey -algorithm ED25519`
key: |
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIIgITVt9BRor5Dn2v7rQu2I8siicIUGr7+QS9PqNSSXk
-----END PRIVATE KEY-----
# - name: bar
# ed25519:
# ## generate with `openssl genpkey -algorithm ED25519 -out ./contrib/bar_ed25519.pem`
# key_file: ./contrib/bar_ed25519.pem

# web:
# tls:
# certificate: "/path/to/server-crt.pem"
# certificate-key: "/path/to/server-key.pem"
# min-protocol-version: "TLSv1.2"
# # max-protocol-version: "TLSv1.3"
# ciphers:
# # - RSA_WITH_RC4_128_SHA
# # - RSA_WITH_3DES_EDE_CBC_SHA
# # - RSA_WITH_AES_128_CBC_SHA
# # - RSA_WITH_AES_256_CBC_SHA
# # - RSA_WITH_AES_128_CBC_SHA256
# # - RSA_WITH_AES_128_GCM_SHA256
# # - RSA_WITH_AES_256_GCM_SHA384
# # - ECDHE_ECDSA_WITH_RC4_128_SHA
# # - ECDHE_ECDSA_WITH_AES_128_CBC_SHA
# # - ECDHE_ECDSA_WITH_AES_256_CBC_SHA
# # - ECDHE_RSA_WITH_RC4_128_SHA
# # - ECDHE_RSA_WITH_3DES_EDE_CBC_SHA
# # - ECDHE_RSA_WITH_AES_128_CBC_SHA
# # - ECDHE_RSA_WITH_AES_256_CBC_SHA
# # - ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
# # - ECDHE_RSA_WITH_AES_128_CBC_SHA256
# - ECDHE_RSA_WITH_AES_128_GCM_SHA256
# # - ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
# - ECDHE_RSA_WITH_AES_256_GCM_SHA384
# # - ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
# - ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
# # - ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
# - TLS_AES_128_GCM_SHA256
# - TLS_AES_256_GCM_SHA384
# - TLS_CHACHA20_POLY1305_SHA256
# prefer-server-ciphers: true
# # ecdh-curves:
# # - secp256r1
# # - secp384r1
# # - secp521r1
# # - x25519
# # session-tickets: true
# # session-ticket-key: "b947e39f50e20351bdd81046e20fff7948d359a3aec391719d60645c5972cc77"
131 changes: 131 additions & 0 deletions cookie/controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
//
// Copyright (c) 2023 whawty contributors (see AUTHORS file)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// * Neither the name of whawty.nginx-sso nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//

package cookie

import (
"fmt"
"time"
)

const (
DefaultExpire = 24 * time.Hour
)

type SignerConfig struct {
Name string `yaml:"name"`
Ed25519 *Ed25519Config `yaml:"ed25519"`
}

type Config struct {
Domain string `yaml:"domain"`
Name string `yaml:"name"`
Secure bool `yaml:"secure"`
Expire time.Duration `yaml:"expire"`
Signers []SignerConfig `yaml:"signers"`
}

type Signer interface {
Sign(payload []byte) ([]byte, error)
Verify(payload, signature []byte) error
}

type Controller struct {
conf *Config
signers []Signer
}

func NewController(conf *Config) (*Controller, error) {
if conf.Name == "" {
conf.Name = "whawty-nginx-sso"
}
if conf.Expire <= 0 {
conf.Expire = DefaultExpire
}

ctrl := &Controller{conf: conf}
for _, sc := range conf.Signers {
var s Signer
if sc.Ed25519 != nil {
var err error
s, err = NewEd25519Signer(conf.Name+"_"+sc.Name, sc.Ed25519)
if err != nil {
return nil, fmt.Errorf("cookies: failed to initialize Ed25519 signer '%s': %v", sc.Name, err)
}
}
if s == nil {
return nil, fmt.Errorf("cookies: failed to initialize signer '%s': no valid type-specific config found", sc.Name)
}
ctrl.signers = append(ctrl.signers, s)
}
if len(ctrl.signers) < 1 {
return nil, fmt.Errorf("cookies: at least one signer must be configured")
}
return ctrl, nil
}

func (c *Controller) Mint(p Payload) (name, value string, err error) {
p.Expires = time.Now().Add(c.conf.Expire).Unix()
v := &Value{payload: p.Encode()}
if v.signature, err = c.signers[0].Sign(v.payload); err != nil {
return
}

name = c.conf.Name
value = v.String()
return
}

func (c *Controller) Verify(value string) (p Payload, err error) {
var v Value
if err = v.FromString(value); err != nil {
return
}

for _, signer := range c.signers {
if err = signer.Verify(v.payload, v.signature); err == nil {
break
}
}
if err != nil {
err = fmt.Errorf("cookie signature is not valid")
return
}

if err = p.Decode(v.payload); err != nil {
err = fmt.Errorf("unable to decode cookie: %v", err)
return
}
if time.Unix(p.Expires, 0).Before(time.Now()) {
err = fmt.Errorf("cookie is expired")
return
}
return
}
77 changes: 77 additions & 0 deletions cookie/cookie.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//
// Copyright (c) 2023 whawty contributors (see AUTHORS file)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// * Neither the name of whawty.nginx-sso nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//

package cookie

import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"
)

type Payload struct {
Username string
Expires int64
}

func (p Payload) Encode() []byte {
payload, _ := json.Marshal(p)
return payload
}

func (p Payload) Decode(payload []byte) error {
return json.Unmarshal(payload, &p)
}

type Value struct {
payload []byte
signature []byte
}

func (v Value) String() string {
return base64.RawURLEncoding.EncodeToString(v.payload) + "." + base64.RawURLEncoding.EncodeToString(v.signature)
}

func (v Value) FromString(encoded string) (err error) {
parts := strings.SplitN(encoded, ".", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid cookie value")
}
v.payload, err = base64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
return fmt.Errorf("invalid cookie value: %v", err)
}
v.signature, err = base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return fmt.Errorf("invalid cookie value: %v", err)
}
return
}
Loading

0 comments on commit 424ca36

Please sign in to comment.