mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
132 lines
3.8 KiB
Go
132 lines
3.8 KiB
Go
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"`
|
|
}
|
|
|
|
// 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.Request.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.Request.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
|
|
}
|