added oauth flow to get authorization code
This commit is contained in:
117
pkg/oauth/state.go
Normal file
117
pkg/oauth/state.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// STATE FLOW:
|
||||
// data provided at call time to be retrieved later
|
||||
// random value generated on the spot
|
||||
// userAgentKey - nonce used to prevent MITM, stored as lax cookie on client
|
||||
// privateKey - from config
|
||||
|
||||
func GenerateState(cfg *Config, data string) (state string, userAgentKey []byte, err error) {
|
||||
// signature = BASE64_SHA256(data + "." + random + userAgentKey + privateKey)
|
||||
// state = data + "." + random + "." + signature
|
||||
if cfg == nil {
|
||||
return "", nil, errors.New("cfg cannot be nil")
|
||||
}
|
||||
if cfg.PrivateKey == "" {
|
||||
return "", nil, errors.New("private key cannot be empty")
|
||||
}
|
||||
if data == "" {
|
||||
return "", nil, errors.New("data cannot be empty")
|
||||
}
|
||||
|
||||
// Generate 32 random bytes for random component
|
||||
randomBytes := make([]byte, 32)
|
||||
_, err = rand.Read(randomBytes)
|
||||
if err != nil {
|
||||
return "", nil, errors.Wrap(err, "failed to generate random bytes")
|
||||
}
|
||||
|
||||
// Generate 32 random bytes for userAgentKey
|
||||
userAgentKey = make([]byte, 32)
|
||||
_, err = rand.Read(userAgentKey)
|
||||
if err != nil {
|
||||
return "", nil, errors.Wrap(err, "failed to generate userAgentKey bytes")
|
||||
}
|
||||
|
||||
// Encode random and userAgentKey to base64
|
||||
randomEncoded := base64.RawURLEncoding.EncodeToString(randomBytes)
|
||||
userAgentKeyEncoded := base64.RawURLEncoding.EncodeToString(userAgentKey)
|
||||
|
||||
// Create payload for signing: data + "." + random + userAgentKey + privateKey
|
||||
// Note: userAgentKey is concatenated directly with privateKey (no separator)
|
||||
payload := data + "." + randomEncoded + userAgentKeyEncoded + cfg.PrivateKey
|
||||
|
||||
// Generate signature
|
||||
hash := sha256.Sum256([]byte(payload))
|
||||
signature := base64.RawURLEncoding.EncodeToString(hash[:])
|
||||
|
||||
// Construct state: data + "." + random + "." + signature
|
||||
state = data + "." + randomEncoded + "." + signature
|
||||
|
||||
return state, userAgentKey, nil
|
||||
}
|
||||
|
||||
func VerifyState(cfg *Config, state string, userAgentKey []byte) (data string, err error) {
|
||||
// Validate inputs
|
||||
if cfg == nil {
|
||||
return "", errors.New("cfg cannot be nil")
|
||||
}
|
||||
if cfg.PrivateKey == "" {
|
||||
return "", errors.New("private key cannot be empty")
|
||||
}
|
||||
if state == "" {
|
||||
return "", errors.New("state cannot be empty")
|
||||
}
|
||||
if len(userAgentKey) == 0 {
|
||||
return "", errors.New("userAgentKey cannot be empty")
|
||||
}
|
||||
|
||||
// Split state into parts
|
||||
parts := strings.Split(state, ".")
|
||||
if len(parts) != 3 {
|
||||
return "", errors.Errorf("state must have exactly 3 parts (data.random.signature), got %d parts", len(parts))
|
||||
}
|
||||
|
||||
// Check for empty parts
|
||||
if slices.Contains(parts, "") {
|
||||
return "", errors.New("state parts cannot be empty")
|
||||
}
|
||||
|
||||
data = parts[0]
|
||||
random := parts[1]
|
||||
receivedSignature := parts[2]
|
||||
|
||||
// Encode userAgentKey to base64 for payload reconstruction
|
||||
userAgentKeyEncoded := base64.RawURLEncoding.EncodeToString(userAgentKey)
|
||||
|
||||
// Reconstruct payload (same as generation): data + "." + random + userAgentKeyEncoded + privateKey
|
||||
payload := data + "." + random + userAgentKeyEncoded + cfg.PrivateKey
|
||||
|
||||
// Generate expected hash
|
||||
hash := sha256.Sum256([]byte(payload))
|
||||
|
||||
// Decode received signature to bytes
|
||||
receivedBytes, err := base64.RawURLEncoding.DecodeString(receivedSignature)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to decode received signature")
|
||||
}
|
||||
|
||||
// Compare hash bytes directly with decoded signature using constant-time comparison
|
||||
// This is more efficient than encoding hash and then decoding both for comparison
|
||||
if subtle.ConstantTimeCompare(hash[:], receivedBytes) == 1 {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
return "", errors.New("invalid state signature")
|
||||
}
|
||||
Reference in New Issue
Block a user