feat(auth): complete sso subsystem integration
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
package wayne
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderService = "X-Wayne-Service"
|
||||
HeaderTimestamp = "X-Wayne-Timestamp"
|
||||
HeaderNonce = "X-Wayne-Nonce"
|
||||
HeaderSignature = "X-Wayne-Signature"
|
||||
|
||||
SignaturePrefix = "sha256="
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingSecret = errors.New("wayne service api secret key is empty")
|
||||
ErrMissingHeader = errors.New("wayne service header value is empty")
|
||||
)
|
||||
|
||||
type SignedHeaders struct {
|
||||
Service string
|
||||
Timestamp string
|
||||
Nonce string
|
||||
Signature string
|
||||
}
|
||||
|
||||
func BodySHA256Hex(body []byte) string {
|
||||
sum := sha256.Sum256(body)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func SigningPayload(method, uri, timestamp, nonce string, body []byte) string {
|
||||
return strings.Join([]string{
|
||||
strings.ToUpper(strings.TrimSpace(method)),
|
||||
uri,
|
||||
timestamp,
|
||||
nonce,
|
||||
BodySHA256Hex(body),
|
||||
}, "\n")
|
||||
}
|
||||
|
||||
func Sign(secret, method, uri, timestamp, nonce string, body []byte) (string, error) {
|
||||
secret = secretValue(secret)
|
||||
if secret == "" {
|
||||
return "", ErrMissingSecret
|
||||
}
|
||||
payload := SigningPayload(method, uri, timestamp, nonce, body)
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
_, _ = mac.Write([]byte(payload))
|
||||
return SignaturePrefix + hex.EncodeToString(mac.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func Verify(secret, signature, method, uri, timestamp, nonce string, body []byte) bool {
|
||||
expected, err := Sign(secret, method, uri, timestamp, nonce, body)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return subtle.ConstantTimeCompare([]byte(normalizeSignature(signature)), []byte(expected)) == 1
|
||||
}
|
||||
|
||||
func BuildSignedHeaders(service, secret, method, uri string, body []byte, now time.Time) (SignedHeaders, error) {
|
||||
service = strings.TrimSpace(service)
|
||||
if service == "" {
|
||||
return SignedHeaders{}, fmt.Errorf("%w: service", ErrMissingHeader)
|
||||
}
|
||||
timestamp := strconv.FormatInt(now.Unix(), 10)
|
||||
nonce, err := NewNonce()
|
||||
if err != nil {
|
||||
return SignedHeaders{}, err
|
||||
}
|
||||
signature, err := Sign(secret, method, uri, timestamp, nonce, body)
|
||||
if err != nil {
|
||||
return SignedHeaders{}, err
|
||||
}
|
||||
return SignedHeaders{
|
||||
Service: service,
|
||||
Timestamp: timestamp,
|
||||
Nonce: nonce,
|
||||
Signature: signature,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h SignedHeaders) Apply(req *http.Request) {
|
||||
req.Header.Set(HeaderService, h.Service)
|
||||
req.Header.Set(HeaderTimestamp, h.Timestamp)
|
||||
req.Header.Set(HeaderNonce, h.Nonce)
|
||||
req.Header.Set(HeaderSignature, h.Signature)
|
||||
}
|
||||
|
||||
func NewNonce() (string, error) {
|
||||
buf := make([]byte, 16)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(buf), nil
|
||||
}
|
||||
|
||||
func normalizeSignature(signature string) string {
|
||||
signature = strings.TrimSpace(signature)
|
||||
if strings.HasPrefix(signature, SignaturePrefix) {
|
||||
return signature
|
||||
}
|
||||
return SignaturePrefix + signature
|
||||
}
|
||||
|
||||
func secretValue(secret string) string {
|
||||
secret = strings.TrimSpace(secret)
|
||||
if _, value, ok := strings.Cut(secret, ":"); ok {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
return secret
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package wayne
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBodySHA256Hex(t *testing.T) {
|
||||
got := BodySHA256Hex([]byte("hello"))
|
||||
want := "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
|
||||
if got != want {
|
||||
t.Fatalf("BodySHA256Hex() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSigningPayload(t *testing.T) {
|
||||
body := []byte(`{"groupIds":[10]}`)
|
||||
got := SigningPayload("put", "/api/v1/internal/namespaces/1/users/2001/roles", "1721000000", "nonce-001", body)
|
||||
want := strings.Join([]string{
|
||||
"PUT",
|
||||
"/api/v1/internal/namespaces/1/users/2001/roles",
|
||||
"1721000000",
|
||||
"nonce-001",
|
||||
"cf296504b2a434969ee151c1a48aa07fabb64634c77e569af74260cf234080f2",
|
||||
}, "\n")
|
||||
if got != want {
|
||||
t.Fatalf("SigningPayload() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignAndVerify(t *testing.T) {
|
||||
body := []byte(`{"groupIds":[10]}`)
|
||||
signature, err := Sign("test-secret", "PUT", "/api/v1/internal/namespaces/1/users/2001/roles", "1721000000", "nonce-001", body)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign() error = %v", err)
|
||||
}
|
||||
want := "sha256=6da70ea095fde90b534d0677da62db867d0b8092f6b15ed86cb52d77571b5b1d"
|
||||
if signature != want {
|
||||
t.Fatalf("Sign() = %q, want %q", signature, want)
|
||||
}
|
||||
if !Verify("test-secret", signature, "PUT", "/api/v1/internal/namespaces/1/users/2001/roles", "1721000000", "nonce-001", body) {
|
||||
t.Fatal("Verify() rejected valid signature")
|
||||
}
|
||||
if !Verify("test-secret", strings.TrimPrefix(signature, SignaturePrefix), "PUT", "/api/v1/internal/namespaces/1/users/2001/roles", "1721000000", "nonce-001", body) {
|
||||
t.Fatal("Verify() rejected valid signature without prefix")
|
||||
}
|
||||
if Verify("test-secret", signature, "PUT", "/api/v1/internal/namespaces/1/users/2001/roles", "1721000000", "nonce-001", []byte(`{"groupIds":[11]}`)) {
|
||||
t.Fatal("Verify() accepted tampered body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignAcceptsServicePrefixedSecret(t *testing.T) {
|
||||
body := []byte(`{"groupIds":[10]}`)
|
||||
plainSecretSignature, err := Sign("test-secret", "PUT", "/api/v1/internal/namespaces/1/users/2001/roles", "1721000000", "nonce-001", body)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign() with plain secret error = %v", err)
|
||||
}
|
||||
prefixedSecretSignature, err := Sign("xinfra:test-secret", "PUT", "/api/v1/internal/namespaces/1/users/2001/roles", "1721000000", "nonce-001", body)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign() with prefixed secret error = %v", err)
|
||||
}
|
||||
if prefixedSecretSignature != plainSecretSignature {
|
||||
t.Fatalf("prefixed secret signature = %q, want %q", prefixedSecretSignature, plainSecretSignature)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSignedHeadersAndApply(t *testing.T) {
|
||||
body := []byte(`{"groupIds":[10]}`)
|
||||
now := time.Unix(1721000000, 0)
|
||||
headers, err := BuildSignedHeaders("xinfra", "test-secret", "PUT", "/api/v1/internal/namespaces/1/users/2001/roles", body, now)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildSignedHeaders() error = %v", err)
|
||||
}
|
||||
if headers.Service != "xinfra" {
|
||||
t.Fatalf("headers.Service = %q, want xinfra", headers.Service)
|
||||
}
|
||||
if headers.Timestamp != "1721000000" {
|
||||
t.Fatalf("headers.Timestamp = %q, want 1721000000", headers.Timestamp)
|
||||
}
|
||||
if headers.Nonce == "" {
|
||||
t.Fatal("headers.Nonce is empty")
|
||||
}
|
||||
if !Verify("test-secret", headers.Signature, "PUT", "/api/v1/internal/namespaces/1/users/2001/roles", headers.Timestamp, headers.Nonce, body) {
|
||||
t.Fatal("generated headers signature is invalid")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPut, "http://wayne.example.com/api/v1/internal/namespaces/1/users/2001/roles", strings.NewReader(string(body)))
|
||||
if err != nil {
|
||||
t.Fatalf("NewRequest() error = %v", err)
|
||||
}
|
||||
headers.Apply(req)
|
||||
if req.Header.Get(HeaderService) != headers.Service ||
|
||||
req.Header.Get(HeaderTimestamp) != headers.Timestamp ||
|
||||
req.Header.Get(HeaderNonce) != headers.Nonce ||
|
||||
req.Header.Get(HeaderSignature) != headers.Signature {
|
||||
t.Fatalf("Apply() did not write expected signed headers: %#v", req.Header)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user