~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
115 lines · 2.5 KB Raw History
package oauthclient

import (
	"cmp"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"strings"
	"time"

	"github.com/bluesky-social/indigo/atproto/atcrypto"
)

const (
	DPoPLifetime = 30 * time.Second

	ReplyLimit = 1 << 20
)

type DPoPHeader struct {
	Typ string        `json:"typ"`
	Alg string        `json:"alg"`
	JWK *atcrypto.JWK `json:"jwk"`
}

type DPoPClaims struct {
	JTI string `json:"jti"`

	HTM string `json:"htm"`

	HTU string `json:"htu"`

	IAT int64 `json:"iat"`

	EXP int64 `json:"exp"`

	ATH string `json:"ath,omitempty"`

	Nonce string `json:"nonce,omitempty"`
}

func DPoPProof(key atcrypto.PrivateKeyExportable, method, target, accessToken, nonce string) (string, error) {
	if key == nil {
		return "", errors.New("oauthclient: no DPoP key")
	}
	public, err := key.PublicKey()
	if err != nil {
		return "", err
	}
	jwk, err := public.JWK()
	if err != nil {
		return "", err
	}
	htu, ok := DPoPHTU(target)
	if !ok {
		return "", fmt.Errorf("oauthclient: the request url %q is not absolute", target)
	}
	jtiRaw := make([]byte, 16)
	if _, err := rand.Read(jtiRaw); err != nil {
		return "", err
	}
	now := time.Now()
	claims := DPoPClaims{
		JTI:   base64.RawURLEncoding.EncodeToString(jtiRaw),
		HTM:   strings.ToUpper(method),
		HTU:   htu,
		IAT:   now.Unix(),
		EXP:   now.Add(DPoPLifetime).Unix(),
		Nonce: nonce,
	}
	if accessToken != "" {
		sum := sha256.Sum256([]byte(accessToken))
		claims.ATH = base64.RawURLEncoding.EncodeToString(sum[:])
	}
	headerJSON, err := json.Marshal(DPoPHeader{Typ: "dpop+jwt", Alg: "ES256", JWK: jwk})
	if err != nil {
		return "", err
	}
	claimsJSON, err := json.Marshal(claims)
	if err != nil {
		return "", err
	}
	signingInput := base64.RawURLEncoding.EncodeToString(headerJSON) + "." + base64.RawURLEncoding.EncodeToString(claimsJSON)
	signature, err := key.HashAndSign([]byte(signingInput))
	if err != nil {
		return "", err
	}
	return signingInput + "." + base64.RawURLEncoding.EncodeToString(signature), nil
}

func DPoPHTU(raw string) (string, bool) {
	parsed, err := url.Parse(raw)
	if err != nil || parsed.Scheme == "" || parsed.Host == "" {
		return "", false
	}
	scheme := strings.ToLower(parsed.Scheme)
	host := strings.ToLower(parsed.Hostname())
	port := parsed.Port()
	if (scheme == "https" && port == "443") || (scheme == "http" && port == "80") {
		port = ""
	}
	if strings.Contains(host, ":") {
		host = "[" + host + "]"
	}
	if port != "" {
		host += ":" + port
	}
	path := parsed.EscapedPath()
	path = cmp.Or(path, "/")
	return scheme + "://" + host + path, true
}