Ctrl K
ComunidadGuías…

coderd/workspaceapps/token.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package workspaceapps

import (
	"net/http"
	"strings"
	"time"

	"github.com/go-jose/go-jose/v4/jwt"
	"github.com/google/uuid"
	"golang.org/x/xerrors"

	"github.com/coder/coder/v2/coderd/cryptokeys"
	"github.com/coder/coder/v2/coderd/jwtutils"
	"github.com/coder/coder/v2/codersdk"
)

// SignedToken is the struct data contained inside a workspace app JWE. It
// contains the details of the workspace app that the token is valid for to
// avoid database queries.
type SignedToken struct {
	jwtutils.RegisteredClaims
	// Request details.
	Request `json:"request"`

	UserID       uuid.UUID             `json:"user_id"`
	WorkspaceID  uuid.UUID             `json:"workspace_id"`
	AgentID      uuid.UUID             `json:"agent_id"`
	AppURL       string                `json:"app_url"`
	CORSBehavior codersdk.CORSBehavior `json:"cors_behavior"`
}

// MatchesRequest returns true if the token matches the request. Any token that
// does not match the request should be considered invalid.
func (t SignedToken) MatchesRequest(req Request) bool {
	tokenBasePath := t.BasePath
	if !strings.HasSuffix(tokenBasePath, "/") {
		tokenBasePath += "/"
	}
	reqBasePath := req.BasePath
	if !strings.HasSuffix(reqBasePath, "/") {
		reqBasePath += "/"
	}

	return t.AccessMethod == req.AccessMethod &&
		tokenBasePath == reqBasePath &&
		t.Prefix == req.Prefix &&
		t.UsernameOrID == req.UsernameOrID &&
		t.WorkspaceNameOrID == req.WorkspaceNameOrID &&
		t.AgentNameOrID == req.AgentNameOrID &&
		t.AppSlugOrPort == req.AppSlugOrPort
}

type EncryptedAPIKeyPayload struct {
	jwtutils.RegisteredClaims
	APIKey string `json:"api_key"`
}

func (e *EncryptedAPIKeyPayload) Fill(now time.Time) {
	e.Issuer = "coderd"
	e.Audience = jwt.Audience{"wsproxy"}
	e.Expiry = jwt.NewNumericDate(now.Add(time.Minute))
	e.NotBefore = jwt.NewNumericDate(now.Add(-time.Minute))
}

func (e EncryptedAPIKeyPayload) Validate(ex jwt.Expected) error {
	if e.NotBefore == nil {
		return xerrors.Errorf("not before is required")
	}

	ex.Issuer = "coderd"
	ex.AnyAudience = jwt.Audience{"wsproxy"}

	return e.RegisteredClaims.Validate(ex)
}

// FromRequest returns the signed token from the request, if it exists and is
// valid. The caller must check that the token matches the request.
func FromRequest(r *http.Request, mgr cryptokeys.SigningKeycache) (*SignedToken, bool) {
	// Get all signed app tokens from the request. This includes the query
	// parameter and all matching cookies sent with the request. If there are
	// somehow multiple signed app token cookies, we want to try all of them
	// (up to 4). The first one that is valid is used.
	//
	// Browsers will send all cookies in the request, even if there are multiple
	// with the same name on different paths.
	//
	// If using a query parameter the request MUST be a terminal request. We use
	// this to support cross-domain terminal access for the web terminal.
	var (
		tokens        = []string{}
		hasQueryParam = false
	)
	if q := r.URL.Query().Get(codersdk.SignedAppTokenQueryParameter); q != "" {
		hasQueryParam = true
		tokens = append(tokens, q)
	}
	for _, cookie := range r.Cookies() {
		if cookie.Name == codersdk.SignedAppTokenCookie {
			tokens = append(tokens, cookie.Value)
		}
	}

	if len(tokens) > 4 {
		tokens = tokens[:4]
	}

	ctx := r.Context()
	for _, tokenStr := range tokens {
		var token SignedToken
		err := jwtutils.Verify(ctx, mgr, tokenStr, &token, jwtutils.WithVerifyExpected(jwt.Expected{
			Time: time.Now(),
		}))
		if err == nil {
			req := token.Normalize()
			if hasQueryParam && req.AccessMethod != AccessMethodTerminal {
				// The request must be a terminal request if we're using a
				// query parameter.
				return nil, false
			}

			err := req.Check()
			if err == nil {
				// The request has a valid signed app token, which is a valid
				// token signed by us. The caller must check that it matches
				// the request.
				return &token, true
			}
		}
	}

	return nil, false
}