Initial QSfera import
This commit is contained in:
+116
@@ -0,0 +1,116 @@
|
||||
package signedurl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// JWTSignedURL implements the Signer and Verifier interfaces using JWT for signing URLs.
|
||||
type JWTSignedURL struct {
|
||||
JWTOptions
|
||||
}
|
||||
|
||||
type claims struct {
|
||||
TargetURL string `json:"target_url"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// JWTOption defines a single option function.
|
||||
type JWTOption func(o *JWTOptions)
|
||||
|
||||
// JWTOptions defines the available options for this package.
|
||||
type JWTOptions struct {
|
||||
secret string // Secret key used for signing and verifying JWTs
|
||||
queryParam string // Name of the query parameter for the signature
|
||||
}
|
||||
|
||||
func NewJWTSignedURL(opts ...JWTOption) (*JWTSignedURL, error) {
|
||||
opt := JWTOptions{}
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
if opt.secret == "" {
|
||||
return nil, ErrInvalidKey
|
||||
}
|
||||
|
||||
if opt.queryParam == "" {
|
||||
opt.queryParam = "oc-jwt-sig"
|
||||
}
|
||||
|
||||
return &JWTSignedURL{opt}, nil
|
||||
}
|
||||
|
||||
func WithSecret(secret string) JWTOption {
|
||||
return func(o *JWTOptions) {
|
||||
o.secret = secret
|
||||
}
|
||||
}
|
||||
|
||||
func WithQueryParam(queryParam string) JWTOption {
|
||||
return func(o *JWTOptions) {
|
||||
o.queryParam = queryParam
|
||||
}
|
||||
}
|
||||
|
||||
// Sign signs a URL using JWT with a specified time-to-live (ttl).
|
||||
func (j *JWTSignedURL) Sign(unsignedURL, subject string, ttl time.Duration) (string, error) {
|
||||
// Re-encode the Query parameters to ensure they are "normalized" (Values.Encode() does return them alphabetically ordered).
|
||||
u, err := url.Parse(unsignedURL)
|
||||
if err != nil {
|
||||
return "", NewSignedURLError(err, "failed to parse url")
|
||||
}
|
||||
query := u.Query()
|
||||
u.RawQuery = query.Encode()
|
||||
c := claims{
|
||||
TargetURL: u.String(),
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(ttl)),
|
||||
Issuer: "reva",
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Subject: subject,
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
|
||||
signedToken, err := token.SignedString([]byte(j.secret))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("signing failed: %w", err)
|
||||
}
|
||||
query.Set(j.queryParam, signedToken)
|
||||
u.RawQuery = query.Encode()
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// Verify verifies a signed URL using a JWT. Returns the subject of the JWT if verification is successful.
|
||||
func (j *JWTSignedURL) Verify(signedURL string) (string, error) {
|
||||
u, err := url.Parse(signedURL)
|
||||
if err != nil {
|
||||
return "", NewSignatureVerificationError(fmt.Errorf("could not parse URL: %w", err))
|
||||
}
|
||||
query := u.Query()
|
||||
tokenString := query.Get(j.queryParam)
|
||||
if tokenString == "" {
|
||||
return "", NewSignatureVerificationError(errors.New("no signature in url"))
|
||||
}
|
||||
token, err := jwt.ParseWithClaims(tokenString, &claims{}, func(token *jwt.Token) (any, error) { return []byte(j.secret), nil })
|
||||
if err != nil {
|
||||
return "", NewSignatureVerificationError(err)
|
||||
}
|
||||
c, ok := token.Claims.(*claims)
|
||||
if !ok {
|
||||
return "", NewSignatureVerificationError(errors.New("invalid JWT claims"))
|
||||
}
|
||||
|
||||
query.Del(j.queryParam)
|
||||
u.RawQuery = query.Encode()
|
||||
|
||||
if c.TargetURL != u.String() {
|
||||
return "", NewSignatureVerificationError(errors.New("url mismatch"))
|
||||
}
|
||||
|
||||
return c.Subject, nil
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
// Package signedurl provides interfaces and implementations for signing and verifying URLs.
|
||||
package signedurl
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Signer interface {
|
||||
// Sign signs a URL
|
||||
Sign(url, principal string, ttl time.Duration) (string, error)
|
||||
}
|
||||
|
||||
type Verifier interface {
|
||||
// Verify verifies a signed URL
|
||||
Verify(signedURL string) (string, error)
|
||||
}
|
||||
|
||||
type SignedURLError struct {
|
||||
innerErr error
|
||||
message string
|
||||
}
|
||||
|
||||
// NewSignedURLError creates a new SignedURLError with the provided inner error and message.
|
||||
func NewSignedURLError(innerErr error, message string) SignedURLError {
|
||||
return SignedURLError{
|
||||
innerErr: innerErr,
|
||||
message: message,
|
||||
}
|
||||
}
|
||||
|
||||
var ErrInvalidKey = NewSignedURLError(nil, "invalid key provided")
|
||||
|
||||
type SignatureVerificationError struct {
|
||||
SignedURLError
|
||||
}
|
||||
|
||||
func NewSignatureVerificationError(innerErr error) SignatureVerificationError {
|
||||
return SignatureVerificationError{
|
||||
SignedURLError: SignedURLError{
|
||||
innerErr: innerErr,
|
||||
message: "signature verification failed",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (e SignatureVerificationError) Is(tgt error) bool {
|
||||
// Check if the target error is of type SignatureVerificationError
|
||||
if _, ok := tgt.(SignatureVerificationError); ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Error implements the error interface for errorConst.
|
||||
func (e SignedURLError) Error() string {
|
||||
if e.innerErr != nil {
|
||||
return e.message + ": " + e.innerErr.Error()
|
||||
}
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (e SignedURLError) Unwrap() error {
|
||||
return e.innerErr
|
||||
}
|
||||
Reference in New Issue
Block a user