2023-03-07 19:38:11 +00:00
|
|
|
package workspaceapps
|
|
|
|
|
|
|
|
import (
|
2023-04-04 00:59:41 +00:00
|
|
|
"context"
|
|
|
|
"net/http"
|
2023-03-07 19:38:11 +00:00
|
|
|
"net/url"
|
2023-03-30 13:24:51 +00:00
|
|
|
"time"
|
2023-03-07 19:38:11 +00:00
|
|
|
|
|
|
|
"cdr.dev/slog"
|
2023-08-18 18:55:43 +00:00
|
|
|
"github.com/coder/coder/v2/codersdk"
|
2023-03-07 19:38:11 +00:00
|
|
|
)
|
|
|
|
|
2023-04-04 00:59:41 +00:00
|
|
|
const (
|
|
|
|
// TODO(@deansheather): configurable expiry
|
|
|
|
DefaultTokenExpiry = time.Minute
|
|
|
|
|
|
|
|
// RedirectURIQueryParam is the query param for the app URL to be passed
|
|
|
|
// back to the API auth endpoint on the main access URL.
|
|
|
|
RedirectURIQueryParam = "redirect_uri"
|
|
|
|
)
|
2023-03-07 19:38:11 +00:00
|
|
|
|
2023-04-17 19:57:21 +00:00
|
|
|
type ResolveRequestOptions struct {
|
|
|
|
Logger slog.Logger
|
|
|
|
SignedTokenProvider SignedTokenProvider
|
|
|
|
|
|
|
|
DashboardURL *url.URL
|
|
|
|
PathAppBaseURL *url.URL
|
|
|
|
AppHostname string
|
|
|
|
|
|
|
|
AppRequest Request
|
|
|
|
// TODO: Replace these 2 fields with a "BrowserURL" field which is used for
|
|
|
|
// redirecting the user back to their initial request after authenticating.
|
|
|
|
// AppPath is the path under the app that was hit.
|
|
|
|
AppPath string
|
|
|
|
// AppQuery is the raw query of the request.
|
|
|
|
AppQuery string
|
|
|
|
}
|
|
|
|
|
|
|
|
func ResolveRequest(rw http.ResponseWriter, r *http.Request, opts ResolveRequestOptions) (*SignedToken, bool) {
|
|
|
|
appReq := opts.AppRequest.Normalize()
|
2023-04-04 00:59:41 +00:00
|
|
|
err := appReq.Validate()
|
|
|
|
if err != nil {
|
2023-04-17 19:57:21 +00:00
|
|
|
// This is a 500 since it's a coder server or proxy that's making this
|
|
|
|
// request struct based on details from the request. The values should
|
|
|
|
// already be validated before they are put into the struct.
|
|
|
|
WriteWorkspaceApp500(opts.Logger, opts.DashboardURL, rw, r, &appReq, err, "invalid app request")
|
2023-04-04 00:59:41 +00:00
|
|
|
return nil, false
|
2023-03-07 19:38:11 +00:00
|
|
|
}
|
|
|
|
|
2023-04-17 19:57:21 +00:00
|
|
|
token, ok := opts.SignedTokenProvider.FromRequest(r)
|
2023-04-04 00:59:41 +00:00
|
|
|
if ok && token.MatchesRequest(appReq) {
|
|
|
|
// The request has a valid signed app token and it matches the request.
|
|
|
|
return token, true
|
2023-03-30 13:24:51 +00:00
|
|
|
}
|
|
|
|
|
2023-04-17 19:57:21 +00:00
|
|
|
issueReq := IssueTokenRequest{
|
|
|
|
AppRequest: appReq,
|
|
|
|
PathAppBaseURL: opts.PathAppBaseURL.String(),
|
|
|
|
AppHostname: opts.AppHostname,
|
2023-08-29 01:34:52 +00:00
|
|
|
SessionToken: AppConnectSessionTokenFromRequest(r, appReq.AccessMethod),
|
2023-04-17 19:57:21 +00:00
|
|
|
AppPath: opts.AppPath,
|
|
|
|
AppQuery: opts.AppQuery,
|
|
|
|
}
|
|
|
|
|
|
|
|
token, tokenStr, ok := opts.SignedTokenProvider.Issue(r.Context(), rw, r, issueReq)
|
2023-04-04 00:59:41 +00:00
|
|
|
if !ok {
|
|
|
|
return nil, false
|
2023-03-07 19:38:11 +00:00
|
|
|
}
|
2023-04-04 00:59:41 +00:00
|
|
|
|
2023-08-29 01:34:52 +00:00
|
|
|
// Write the signed app token cookie.
|
|
|
|
//
|
|
|
|
// For path apps, this applies to only the path app base URL on the current
|
|
|
|
// domain, e.g.
|
|
|
|
// /@user/workspace[.agent]/apps/path-app/
|
|
|
|
//
|
|
|
|
// For subdomain apps, this applies to the entire subdomain, e.g.
|
|
|
|
// app--agent--workspace--user.apps.example.com
|
2023-04-04 00:59:41 +00:00
|
|
|
http.SetCookie(rw, &http.Cookie{
|
2023-08-29 01:34:52 +00:00
|
|
|
Name: codersdk.SignedAppTokenCookie,
|
2023-04-04 00:59:41 +00:00
|
|
|
Value: tokenStr,
|
|
|
|
Path: appReq.BasePath,
|
|
|
|
Expires: token.Expiry,
|
|
|
|
})
|
|
|
|
|
|
|
|
return token, true
|
|
|
|
}
|
|
|
|
|
|
|
|
// SignedTokenProvider provides signed workspace app tokens (aka. app tickets).
|
|
|
|
type SignedTokenProvider interface {
|
2023-04-17 19:57:21 +00:00
|
|
|
// FromRequest returns a parsed token from the request. If the request does
|
|
|
|
// not contain a signed app token or is is invalid (expired, invalid
|
2023-04-04 00:59:41 +00:00
|
|
|
// signature, etc.), it returns false.
|
2023-04-17 19:57:21 +00:00
|
|
|
FromRequest(r *http.Request) (*SignedToken, bool)
|
|
|
|
// Issue mints a new token for the given app request. It uses the long-lived
|
|
|
|
// session token in the HTTP request to authenticate and authorize the
|
|
|
|
// client for the given workspace app. The token is returned in struct and
|
|
|
|
// string form. The string form should be written as a cookie.
|
2023-04-04 00:59:41 +00:00
|
|
|
//
|
|
|
|
// If the request is invalid or the user is not authorized to access the
|
|
|
|
// app, false is returned. An error page is written to the response writer
|
|
|
|
// in this case.
|
2023-04-17 19:57:21 +00:00
|
|
|
Issue(ctx context.Context, rw http.ResponseWriter, r *http.Request, appReq IssueTokenRequest) (*SignedToken, string, bool)
|
2023-03-07 19:38:11 +00:00
|
|
|
}
|