(web) csrf protection of public API

This commit is contained in:
Alexey Kontsevoy 2017-10-02 00:01:13 -04:00
parent 763b92f0c3
commit 07783385d3
14 changed files with 500 additions and 103 deletions

141
lib/httplib/csrf/csrf.go Normal file
View file

@ -0,0 +1,141 @@
/*
Copyright 2016 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package csrf
import (
"crypto/subtle"
"encoding/hex"
"net/http"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/trace"
)
const (
// CookieName is a name of the cookie
CookieName = "grv_csrf"
// HeaderName is the default HTTP request header to inspect
HeaderName = "X-CSRF-Token"
// tokenLenBytes is CSRF token length in bytes.
tokenLenBytes = 32
// defaultMaxAge is the default MaxAge for cookies.
defaultMaxAge = 0
)
// AddCSRFProtection adds CSRF token into the user session via secure cookie,
// it implements "double submit cookie" approach to check against CSRF attacks
// https://www.owasp.org/index.php/Cross-Site_Request_Forgery_%28CSRF%29_Prevention_Cheat_Sheet#Double_Submit_Cookie
func AddCSRFProtection(w http.ResponseWriter, r *http.Request) (string, error) {
encodedToken := ""
token, err := extractFromCookie(r)
// if there was an error retrieving the token, the token doesn't exist
if err != nil || len(token) == 0 {
encodedToken, err = utils.CryptoRandomHex(tokenLenBytes)
if err != nil {
return "", trace.Wrap(err)
}
} else {
encodedToken = hex.EncodeToString(token)
}
save(encodedToken, w)
return encodedToken, nil
}
// VerifyToken checks if the cookie value and request value match.
func VerifyToken(w http.ResponseWriter, r *http.Request) error {
realToken, err := extractFromCookie(r)
if err != nil {
return trace.BadParameter("cannot retrieve CSRF token from cookie", err)
}
if len(realToken) != tokenLenBytes {
return trace.BadParameter("invalid CSRF cookie token length, expected %v, got %v", tokenLenBytes, len(realToken))
}
requestToken, err := extractFromRequest(r)
if err != nil {
return trace.BadParameter("cannot retrieve CSRF token from HTTP header", err)
}
// compare the request token against the real token
if !compareTokens(requestToken, realToken) {
return trace.BadParameter("request and cookie CSRF tokens do not match")
}
return nil
}
// extractFromCookie retrieves a CSRF token from the session cookie.
func extractFromCookie(r *http.Request) ([]byte, error) {
cookie, err := r.Cookie(CookieName)
if err != nil {
return nil, trace.Wrap(err)
}
token, err := decode(cookie.Value)
if err != nil {
return nil, trace.Wrap(err)
}
return token, nil
}
// extractFromRequest returns the issued token from HTTP header.
func extractFromRequest(r *http.Request) ([]byte, error) {
issued := r.Header.Get(HeaderName)
decoded, err := decode(issued)
if err != nil {
return nil, trace.Wrap(err)
}
return decoded, nil
}
// save stores encoded CSRF token in the session cookie.
func save(encodedToken string, w http.ResponseWriter) string {
cookie := &http.Cookie{
Name: CookieName,
Value: encodedToken,
MaxAge: defaultMaxAge,
HttpOnly: true,
Secure: true,
Path: "/",
}
// write the authenticated cookie to the response.
http.SetCookie(w, cookie)
w.Header().Add("Vary", "Cookie")
return encodedToken
}
// compare securely (constant-time) compares request token against the real token
// from the session.
func compareTokens(a, b []byte) bool {
// this is required as subtle.ConstantTimeCompare does not check for equal
// lengths in Go versions prior to 1.3.
if len(a) != len(b) {
return false
}
return subtle.ConstantTimeCompare(a, b) == 1
}
// decode decodes a cookie using base64.
func decode(value string) ([]byte, error) {
return hex.DecodeString(value)
}

View file

@ -40,6 +40,7 @@ import (
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/httplib"
"github.com/gravitational/teleport/lib/httplib/csrf"
"github.com/gravitational/teleport/lib/reversetunnel"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/session"
@ -150,7 +151,7 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) {
h.GET("/webapi/ping/:connector", httplib.MakeHandler(h.pingWithConnector))
// Web sessions
h.POST("/webapi/sessions", httplib.MakeHandler(h.createSession))
h.POST("/webapi/sessions", h.WithCSRFProtection(httplib.MakeHandler(h.createSession)))
h.DELETE("/webapi/sessions", h.WithAuth(h.deleteSession))
h.POST("/webapi/sessions/renew", h.WithAuth(h.renewSession))
@ -259,13 +260,24 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) {
// serve Web UI:
if strings.HasPrefix(r.URL.Path, "/web/app") {
http.StripPrefix("/web", http.FileServer(staticFS)).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/web/config.js") {
writeSettings.ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/web") {
ctx, err := h.AuthenticateRequest(w, r, false)
csrfToken, err := csrf.AddCSRFProtection(w, r)
if err != nil {
log.Errorf("failed to generate CSRF token %v", err)
}
session := struct {
Session string
}{Session: base64.StdEncoding.EncodeToString([]byte("{}"))}
XCSRF string
}{
XCSRF: csrfToken,
Session: base64.StdEncoding.EncodeToString([]byte("{}")),
}
ctx, err := h.AuthenticateRequest(w, r, false)
if err == nil {
re, err := NewSessionResponse(ctx)
if err == nil {
@ -281,11 +293,13 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) {
http.NotFound(w, r)
}
})
h.NotFound = routingHandler
plugin := GetPlugin()
if plugin != nil {
plugin.AddHandlers(h)
}
return &RewritingHandler{
Handler: httplib.RewritePaths(h,
httplib.Rewrite("/webapi/sites/([^/]+)/sessions/(.*)", "/webapi/sites/$1/namespaces/default/sessions/$2"),
@ -298,11 +312,11 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) {
}
// Close closes associated session cache operations
func (m *Handler) Close() error {
return m.auth.Close()
func (h *Handler) Close() error {
return h.auth.Close()
}
func (m *Handler) getUserStatus(w http.ResponseWriter, r *http.Request, _ httprouter.Params, c *SessionContext) (interface{}, error) {
func (h *Handler) getUserStatus(w http.ResponseWriter, r *http.Request, _ httprouter.Params, c *SessionContext) (interface{}, error) {
return ok(), nil
}
@ -310,7 +324,7 @@ func (m *Handler) getUserStatus(w http.ResponseWriter, r *http.Request, _ httpro
//
// GET /webapi/user/context
//
func (m *Handler) getUserContext(w http.ResponseWriter, r *http.Request, _ httprouter.Params, c *SessionContext) (interface{}, error) {
func (h *Handler) getUserContext(w http.ResponseWriter, r *http.Request, _ httprouter.Params, c *SessionContext) (interface{}, error) {
clt, err := c.GetClient()
if err != nil {
return nil, trace.Wrap(err)
@ -436,10 +450,10 @@ func defaultAuthenticationSettings(authClient auth.ClientI) (client.Authenticati
return as, nil
}
func (m *Handler) ping(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
func (h *Handler) ping(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var err error
defaultSettings, err := defaultAuthenticationSettings(m.cfg.ProxyClient)
defaultSettings, err := defaultAuthenticationSettings(h.cfg.ProxyClient)
if err != nil {
return nil, trace.Wrap(err)
}
@ -450,8 +464,8 @@ func (m *Handler) ping(w http.ResponseWriter, r *http.Request, p httprouter.Para
}, nil
}
func (m *Handler) pingWithConnector(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
authClient := m.cfg.ProxyClient
func (h *Handler) pingWithConnector(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
authClient := h.cfg.ProxyClient
connectorName := p.ByName("connector")
cap, err := authClient.GetAuthPreference()
@ -460,7 +474,7 @@ func (m *Handler) pingWithConnector(w http.ResponseWriter, r *http.Request, p ht
}
if connectorName == teleport.Local {
as, err := localSettings(m.cfg.ProxyClient, cap)
as, err := localSettings(h.cfg.ProxyClient, cap)
if err != nil {
return nil, trace.Wrap(err)
}
@ -501,8 +515,8 @@ type webConfig struct {
}
// getConfigurationSettings returns configuration for the web application.
func (m *Handler) getConfigurationSettings(w http.ResponseWriter, r *http.Request) (interface{}, error) {
as, err := defaultAuthenticationSettings(m.cfg.ProxyClient)
func (h *Handler) getConfigurationSettings(w http.ResponseWriter, r *http.Request) (interface{}, error) {
as, err := defaultAuthenticationSettings(h.cfg.ProxyClient)
if err != nil {
log.Infof("Cannot retrieve cluster auth preferences: %v", err)
}
@ -521,7 +535,7 @@ func (m *Handler) getConfigurationSettings(w http.ResponseWriter, r *http.Reques
return nil, nil
}
func (m *Handler) oidcLoginWeb(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
func (h *Handler) oidcLoginWeb(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
log.Infof("oidcLoginWeb start")
query := r.URL.Query()
@ -533,7 +547,7 @@ func (m *Handler) oidcLoginWeb(w http.ResponseWriter, r *http.Request, p httprou
if connectorID == "" {
return nil, trace.BadParameter("missing connector_id query parameter")
}
response, err := m.cfg.ProxyClient.CreateOIDCAuthRequest(
response, err := h.cfg.ProxyClient.CreateOIDCAuthRequest(
services.OIDCAuthRequest{
ConnectorID: connectorID,
CreateWebSession: true,
@ -547,7 +561,7 @@ func (m *Handler) oidcLoginWeb(w http.ResponseWriter, r *http.Request, p httprou
return nil, nil
}
func (m *Handler) oidcLoginConsole(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
func (h *Handler) oidcLoginConsole(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
log.Debugf("oidcLoginConsole start")
var req *client.SSOLoginConsoleReq
if err := httplib.ReadJSON(r, &req); err != nil {
@ -562,7 +576,7 @@ func (m *Handler) oidcLoginConsole(w http.ResponseWriter, r *http.Request, p htt
if req.ConnectorID == "" {
return nil, trace.BadParameter("missing ConnectorID")
}
response, err := m.cfg.ProxyClient.CreateOIDCAuthRequest(
response, err := h.cfg.ProxyClient.CreateOIDCAuthRequest(
services.OIDCAuthRequest{
ConnectorID: req.ConnectorID,
ClientRedirectURL: req.RedirectURL,
@ -577,10 +591,10 @@ func (m *Handler) oidcLoginConsole(w http.ResponseWriter, r *http.Request, p htt
return &client.SSOLoginConsoleResponse{RedirectURL: response.RedirectURL}, nil
}
func (m *Handler) oidcCallback(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
func (h *Handler) oidcCallback(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
log.Debugf("oidcCallback start")
response, err := m.cfg.ProxyClient.ValidateOIDCAuthCallback(r.URL.Query())
response, err := h.cfg.ProxyClient.ValidateOIDCAuthCallback(r.URL.Query())
if err != nil {
log.Warningf("[OIDC] Error while processing callback: %v", err)
@ -757,7 +771,7 @@ func NewSessionResponse(ctx *SessionContext) (*CreateSessionResponse, error) {
//
// {"type": "bearer", "token": "bearer token", "user": {"name": "alex", "allowed_logins": ["admin", "bob"]}, "expires_in": 20}
//
func (m *Handler) createSession(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
func (h *Handler) createSession(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var req *createSessionReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
@ -765,7 +779,7 @@ func (m *Handler) createSession(w http.ResponseWriter, r *http.Request, p httpro
// get cluster preferences to see if we should login
// with password or password+otp
authClient := m.cfg.ProxyClient
authClient := h.cfg.ProxyClient
cap, err := authClient.GetAuthPreference()
if err != nil {
return nil, trace.Wrap(err)
@ -775,9 +789,9 @@ func (m *Handler) createSession(w http.ResponseWriter, r *http.Request, p httpro
switch cap.GetSecondFactor() {
case teleport.OFF:
webSession, err = m.auth.AuthWithoutOTP(req.User, req.Pass)
webSession, err = h.auth.AuthWithoutOTP(req.User, req.Pass)
case teleport.OTP, teleport.HOTP, teleport.TOTP:
webSession, err = m.auth.AuthWithOTP(req.User, req.Pass, req.SecondFactorToken)
webSession, err = h.auth.AuthWithOTP(req.User, req.Pass, req.SecondFactorToken)
default:
return nil, trace.AccessDenied("unknown second factor type: %q", cap.GetSecondFactor())
}
@ -789,7 +803,7 @@ func (m *Handler) createSession(w http.ResponseWriter, r *http.Request, p httpro
return nil, trace.Wrap(err)
}
ctx, err := m.auth.ValidateSession(req.User, webSession.GetName())
ctx, err := h.auth.ValidateSession(req.User, webSession.GetName())
if err != nil {
return nil, trace.AccessDenied("need auth")
}
@ -805,7 +819,7 @@ func (m *Handler) createSession(w http.ResponseWriter, r *http.Request, p httpro
//
// {"message": "ok"}
//
func (m *Handler) deleteSession(w http.ResponseWriter, r *http.Request, _ httprouter.Params, ctx *SessionContext) (interface{}, error) {
func (h *Handler) deleteSession(w http.ResponseWriter, r *http.Request, _ httprouter.Params, ctx *SessionContext) (interface{}, error) {
if err := ctx.Invalidate(); err != nil {
return nil, trace.Wrap(err)
}
@ -826,7 +840,7 @@ func (m *Handler) deleteSession(w http.ResponseWriter, r *http.Request, _ httpro
// {"type": "bearer", "token": "bearer token", "user": {"name": "alex", "allowed_logins": ["admin", "bob"]}, "expires_in": 20}
//
//
func (m *Handler) renewSession(w http.ResponseWriter, r *http.Request, _ httprouter.Params, ctx *SessionContext) (interface{}, error) {
func (h *Handler) renewSession(w http.ResponseWriter, r *http.Request, _ httprouter.Params, ctx *SessionContext) (interface{}, error) {
newSess, err := ctx.ExtendWebSession()
if err != nil {
return nil, trace.Wrap(err)
@ -859,9 +873,9 @@ type renderUserInviteResponse struct {
// {"invite_token": "token", "user": "alex", qr: "base64-encoded-qr-code image"}
//
//
func (m *Handler) renderUserInvite(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
func (h *Handler) renderUserInvite(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
token := p[0].Value
user, qrCodeBytes, err := m.auth.GetUserInviteInfo(token)
user, qrCodeBytes, err := h.auth.GetUserInviteInfo(token)
if err != nil {
return nil, trace.Wrap(err)
}
@ -881,9 +895,9 @@ func (m *Handler) renderUserInvite(w http.ResponseWriter, r *http.Request, p htt
//
// {"version":"U2F_V2","challenge":"randombase64string","appId":"https://mycorp.com:3080"}
//
func (m *Handler) u2fRegisterRequest(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
func (h *Handler) u2fRegisterRequest(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
token := p[0].Value
u2fRegisterRequest, err := m.auth.GetUserInviteU2FRegisterRequest(token)
u2fRegisterRequest, err := h.auth.GetUserInviteU2FRegisterRequest(token)
if err != nil {
return nil, trace.Wrap(err)
}
@ -901,12 +915,12 @@ func (m *Handler) u2fRegisterRequest(w http.ResponseWriter, r *http.Request, p h
//
// {"version":"U2F_V2","challenge":"randombase64string","keyHandle":"longbase64string","appId":"https://mycorp.com:3080"}
//
func (m *Handler) u2fSignRequest(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
func (h *Handler) u2fSignRequest(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var req *client.U2fSignRequestReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
u2fSignReq, err := m.auth.GetU2FSignRequest(req.User, req.Pass)
u2fSignReq, err := h.auth.GetU2FSignRequest(req.User, req.Pass)
if err != nil {
return nil, trace.AccessDenied("bad auth credentials")
}
@ -930,20 +944,20 @@ type u2fSignResponseReq struct {
//
// {"type": "bearer", "token": "bearer token", "user": {"name": "alex", "allowed_logins": ["admin", "bob"]}, "expires_in": 20}
//
func (m *Handler) createSessionWithU2FSignResponse(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
func (h *Handler) createSessionWithU2FSignResponse(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var req *u2fSignResponseReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
sess, err := m.auth.AuthWithU2FSignResponse(req.User, &req.U2FSignResponse)
sess, err := h.auth.AuthWithU2FSignResponse(req.User, &req.U2FSignResponse)
if err != nil {
return nil, trace.AccessDenied("bad auth credentials")
}
if err := SetSession(w, req.User, sess.GetName()); err != nil {
return nil, trace.Wrap(err)
}
ctx, err := m.auth.ValidateSession(req.User, sess.GetName())
ctx, err := h.auth.ValidateSession(req.User, sess.GetName())
if err != nil {
return nil, trace.AccessDenied("need auth")
}
@ -966,16 +980,16 @@ type createNewUserReq struct {
// Sucessful response: (session cookie is set)
//
// {"type": "bearer", "token": "bearer token", "user": "alex", "expires_in": 20}
func (m *Handler) createNewUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
func (h *Handler) createNewUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var req *createNewUserReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
sess, err := m.auth.CreateNewUser(req.InviteToken, req.Pass, req.SecondFactorToken)
sess, err := h.auth.CreateNewUser(req.InviteToken, req.Pass, req.SecondFactorToken)
if err != nil {
return nil, trace.Wrap(err)
}
ctx, err := m.auth.ValidateSession(sess.GetUser(), sess.GetName())
ctx, err := h.auth.ValidateSession(sess.GetUser(), sess.GetName())
if err != nil {
return nil, trace.Wrap(err)
}
@ -1001,16 +1015,16 @@ type createNewU2FUserReq struct {
// Sucessful response: (session cookie is set)
//
// {"type": "bearer", "token": "bearer token", "user": "alex", "expires_in": 20}
func (m *Handler) createNewU2FUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
func (h *Handler) createNewU2FUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var req *createNewU2FUserReq
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
sess, err := m.auth.CreateNewU2FUser(req.InviteToken, req.Pass, req.U2FRegisterResponse)
sess, err := h.auth.CreateNewU2FUser(req.InviteToken, req.Pass, req.U2FRegisterResponse)
if err != nil {
return nil, trace.Wrap(err)
}
ctx, err := m.auth.ValidateSession(sess.GetUser(), sess.GetName())
ctx, err := h.auth.ValidateSession(sess.GetUser(), sess.GetName())
if err != nil {
return nil, trace.Wrap(err)
}
@ -1068,7 +1082,7 @@ Sucessful response:
{"namespaces": [{..namespace resource...}]}
*/
func (m *Handler) getSiteNamespaces(w http.ResponseWriter, r *http.Request, _ httprouter.Params, c *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
func (h *Handler) getSiteNamespaces(w http.ResponseWriter, r *http.Request, _ httprouter.Params, c *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
log.Debugf("[web] GET /namespaces")
clt, err := site.GetClient()
if err != nil {
@ -1088,7 +1102,7 @@ type nodeWithSessions struct {
Sessions []session.Session `json:"sessions"`
}
func (m *Handler) siteNodesGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, c *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
func (h *Handler) siteNodesGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, c *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
log.Debugf("[web] GET /nodes")
clt, err := site.GetClient()
if err != nil {
@ -1120,7 +1134,7 @@ func (m *Handler) siteNodesGet(w http.ResponseWriter, r *http.Request, p httprou
//
// Sucessful response is a websocket stream that allows read write to the server
//
func (m *Handler) siteNodeConnect(
func (h *Handler) siteNodeConnect(
w http.ResponseWriter,
r *http.Request,
p httprouter.Params,
@ -1146,7 +1160,7 @@ func (m *Handler) siteNodeConnect(
req.Namespace, req.Server, req.Login, req.SessionID)
req.Namespace = namespace
req.ProxyHostPort = m.ProxyHostPort()
req.ProxyHostPort = h.ProxyHostPort()
req.Cluster = site.GetName()
clt, err := ctx.GetUserClient(site)
@ -1182,7 +1196,7 @@ type sessionStreamEvent struct {
// Sucessful response is a websocket stream that allows read write to the server and returns
// json events
//
func (m *Handler) siteSessionStream(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
func (h *Handler) siteSessionStream(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
sessionID, err := session.ParseID(p.ByName("sid"))
if err != nil {
return nil, trace.Wrap(err)
@ -1194,7 +1208,7 @@ func (m *Handler) siteSessionStream(w http.ResponseWriter, r *http.Request, p ht
}
connect, err := newSessionStreamHandler(namespace,
*sessionID, ctx, site, m.sessionStreamPollPeriod)
*sessionID, ctx, site, h.sessionStreamPollPeriod)
if err != nil {
return nil, trace.Wrap(err)
}
@ -1230,7 +1244,7 @@ type siteSessionGenerateResponse struct {
//
// {"session": {"id": "session-id", "terminal_params": {"w": 100, "h": 100}, "login": "centos"}}
//
func (m *Handler) siteSessionGenerate(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
func (h *Handler) siteSessionGenerate(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
namespace := p.ByName("namespace")
if !services.IsValidNamespace(namespace) {
return nil, trace.BadParameter("invalid namespace %q", namespace)
@ -1264,7 +1278,7 @@ type siteSessionUpdateReq struct {
//
// {"message": "ok"}
//
func (m *Handler) siteSessionUpdate(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
func (h *Handler) siteSessionUpdate(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
sessionID, err := session.ParseID(p.ByName("sid"))
if err != nil {
return nil, trace.Wrap(err)
@ -1306,7 +1320,7 @@ type siteSessionsGetResponse struct {
// Response body:
//
// {"sessions": [{"id": "sid", "terminal_params": {"w": 100, "h": 100}, "parties": [], "login": "bob"}, ...] }
func (m *Handler) siteSessionsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
func (h *Handler) siteSessionsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
clt, err := ctx.GetUserClient(site)
if err != nil {
return nil, trace.Wrap(err)
@ -1332,7 +1346,7 @@ func (m *Handler) siteSessionsGet(w http.ResponseWriter, r *http.Request, p http
//
// {"session": {"id": "sid", "terminal_params": {"w": 100, "h": 100}, "parties": [], "login": "bob"}}
//
func (m *Handler) siteSessionGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
func (h *Handler) siteSessionGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
sessionID, err := session.ParseID(p.ByName("sid"))
if err != nil {
return nil, trace.Wrap(err)
@ -1369,7 +1383,7 @@ const maxStreamBytes = 5 * 1024 * 1024
// the default backend performs exact search: ?key=value means "event
// with a field 'key' with value 'value'
//
func (m *Handler) siteEventsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
func (h *Handler) siteEventsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
query := r.URL.Query()
log.Infof("web.getEvents(%v)", r.URL.RawQuery)
@ -1422,14 +1436,14 @@ type siteSessionStreamGetResponse struct {
// It returns the binary stream unencoded, directly in the respose body,
// with Content-Type of application/octet-stream, gzipped with up to 95%
// compression ratio.
func (m *Handler) siteSessionStreamGet(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
func (h *Handler) siteSessionStreamGet(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
var site reversetunnel.RemoteSite
onError := func(err error) {
w.Header().Set("Content-Type", "text/json")
trace.WriteError(w, err)
}
// authenticate first:
ctx, err := m.AuthenticateRequest(w, r, true)
ctx, err := h.AuthenticateRequest(w, r, true)
if err != nil {
log.Info(err)
// clear session just in case if the authentication request is not valid
@ -1440,14 +1454,14 @@ func (m *Handler) siteSessionStreamGet(w http.ResponseWriter, r *http.Request, p
// get the site interface:
siteName := p.ByName("site")
if siteName == currentSiteShortcut {
sites := m.cfg.Proxy.GetSites()
sites := h.cfg.Proxy.GetSites()
if len(sites) < 1 {
onError(trace.NotFound("no active sites"))
return
}
siteName = sites[0].GetName()
}
site, err = m.cfg.Proxy.GetSite(siteName)
site, err = h.cfg.Proxy.GetSite(siteName)
if err != nil {
onError(err)
return
@ -1525,7 +1539,7 @@ type eventsListGetResponse struct {
//
// {"events": [{...}, {...}, ...}
//
func (m *Handler) siteSessionEventsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
func (h *Handler) siteSessionEventsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
sessionID, err := session.ParseID(p.ByName("sid"))
if err != nil {
return nil, trace.Wrap(err)
@ -1717,7 +1731,19 @@ func (h *Handler) WithAuth(fn ContextHandler) httprouter.Handle {
})
}
// authenticateRequest authenticates request using combination of a session cookie
// WithCSRFProtection ensures that request to unauthenticated API is checked against CSRF attacks
func (h *Handler) WithCSRFProtection(fn httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
err := csrf.VerifyToken(w, r)
if err != nil {
trace.WriteError(w, trace.AccessDenied("failed to validate CSRF token", err))
return
}
fn(w, r, p)
}
}
// AuthenticateRequest authenticates request using combination of a session cookie
// and bearer token
func (h *Handler) AuthenticateRequest(w http.ResponseWriter, r *http.Request, checkBearerToken bool) (*SessionContext, error) {
const missingCookieMsg = "missing session cookie"
@ -1778,10 +1804,6 @@ func ok() interface{} {
return message("ok")
}
type Server struct {
http.Server
}
// CreateSignupLink generates and returns a URL which is given to a new
// user to complete registration with Teleport via Web UI
func CreateSignupLink(hostPort string, token string) string {

View file

@ -49,6 +49,8 @@ import (
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/fixtures"
"github.com/gravitational/teleport/lib/httplib"
"github.com/gravitational/teleport/lib/httplib/csrf"
"github.com/gravitational/teleport/lib/reversetunnel"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/services/local"
@ -589,12 +591,14 @@ func (s *WebSuite) authPack(c *C) *authPack {
c.Assert(err, IsNil)
clt := s.client()
re, err := clt.PostJSON(clt.Endpoint("webapi", "sessions"), createSessionReq{
req := createSessionReq{
User: user,
Pass: pass,
SecondFactorToken: validToken,
})
}
csrfToken := "2ebcb768d0090ea4368e42880c970b61865c326172a4a2343b645cf5d7f20992"
re, err := s.login(clt, csrfToken, csrfToken, req)
c.Assert(err, IsNil)
var rawSess *createSessionResponseRaw
@ -648,6 +652,30 @@ func (s *WebSuite) TestNamespace(c *C) {
c.Assert(err, IsNil)
}
func (s *WebSuite) TestCRSF(c *C) {
type input struct {
reqToken string
cookieToken string
}
encodedToken1 := "2ebcb768d0090ea4368e42880c970b61865c326172a4a2343b645cf5d7f20992"
encodedToken2 := "bf355921bbf3ef3672a03e410d4194077dfa5fe863c652521763b3e7f81e7b11"
invalid := []input{
{reqToken: encodedToken2, cookieToken: encodedToken1},
{reqToken: "", cookieToken: encodedToken1},
{reqToken: "", cookieToken: ""},
{reqToken: encodedToken1, cookieToken: ""},
}
clt := s.client()
// invalid
for i := range invalid {
_, err := s.login(clt, invalid[i].cookieToken, invalid[i].reqToken, nil)
c.Assert(err, NotNil)
c.Assert(trace.IsAccessDenied(err), Equals, true)
}
}
func (s *WebSuite) TestWebSessionsRenew(c *C) {
pack := s.authPack(c)
@ -1344,3 +1372,23 @@ func (s *WebSuite) makeTerminalHandler(login string, server string, v2Servers []
}), nil)
}
func (s *WebSuite) login(clt *client.WebClient, cookieToken string, reqToken string, reqData interface{}) (*roundtrip.Response, error) {
return httplib.ConvertResponse(clt.RoundTrip(func() (*http.Response, error) {
data, err := json.Marshal(reqData)
req, err := http.NewRequest("POST", clt.Endpoint("webapi", "sessions"), bytes.NewBuffer(data))
if err != nil {
return nil, err
}
cookie := &http.Cookie{
Name: csrf.CookieName,
Value: cookieToken,
}
req.AddCookie(cookie)
req.Header.Set("Content-Type", "application/json")
req.Header.Set(csrf.HeaderName, reqToken)
return clt.HTTPClient().Do(req)
}))
}

View file

@ -60,8 +60,6 @@ func (s *StaticSuite) TestZipFS(c *check.C) {
fs, err := readZipArchive("../../fixtures/assets.zip")
c.Assert(err, check.IsNil)
c.Assert(fs, check.NotNil)
checkFS(fs, c)
}
func checkFS(fs http.FileSystem, c *check.C) {
@ -70,7 +68,7 @@ func checkFS(fs http.FileSystem, c *check.C) {
c.Assert(err, check.IsNil)
bytes, err := ioutil.ReadAll(f)
c.Assert(err, check.IsNil)
c.Assert(len(bytes), check.Equals, 813)
c.Assert(len(bytes), check.Equals, 880)
c.Assert(f.Close(), check.IsNil)
// seek + read
@ -84,7 +82,7 @@ func checkFS(fs http.FileSystem, c *check.C) {
bytes, err = ioutil.ReadAll(f)
c.Assert(err, check.IsNil)
c.Assert(len(bytes), check.Equals, 803)
c.Assert(len(bytes), check.Equals, 870)
n, err = f.Seek(-50, io.SeekEnd)
c.Assert(err, check.IsNil)

View file

@ -1,13 +1,13 @@
## web client
Build (to create new /dist files)
To build and generate a new /dist
1. `install nodejs >= 5.0.0`
2. `npm install`
3. `npm run build`
1. `install nodejs >= 8.0.0`
2. `$ npm install`
3. `$ npm run build`
To run a dev server (development)
To run a dev server
1. `npm run start`
1. `$ npm run start -- --proxy=https://host:port`
2. `open https://localhost:8081/web`

View file

@ -14,58 +14,118 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var fs = require('fs');
var uri = require('url');
var WebpackDevServer = require("webpack-dev-server");
var webpackConfig = require('./webpack/webpack.config.dev.js');
var express = require('express');
var webpack = require('webpack');
var proxy = require('http-proxy').createProxyServer();
var changeProxyResponse = require('./devServerUtils');
var PROXY_TARGET = '0.0.0.0:5080/';
// parse target URL
var argv = require('optimist')
.usage('Usage: $0 -proxy [url]')
.demand(['proxy'])
.argv;
var urlObj = uri.parse(argv.proxy)
if (!urlObj.host) {
console.error('invalid URL: ' + argv.proxy);
return;
}
var PROXY_TARGET = urlObj.host;
var ROOT = '/web';
var PORT = '8081';
var WEBPACK_CLIENT_ENTRY = 'webpack-dev-server/client?https://localhost:' + PORT;
var WEBPACK_CLIENT_ENTRY = 'webpack-dev-server/client?https://0.0.0.0:' + PORT;
var WEBPACK_SRV_ENTRY = 'webpack/hot/dev-server';
webpackConfig.entry.app.unshift(WEBPACK_CLIENT_ENTRY, WEBPACK_SRV_ENTRY);
webpackConfig.entry.styles.unshift(WEBPACK_CLIENT_ENTRY, WEBPACK_SRV_ENTRY);
var compiler = webpack(webpackConfig);
function getTargetOptions() {
return {
target: "https://"+PROXY_TARGET,
secure: false,
changeOrigin: true
changeOrigin: true,
xfwd: true
}
}
var compiler = webpack(webpackConfig);
var server = new WebpackDevServer(compiler, {
proxy: {
'/web/config.js': getTargetOptions(),
'/v1/webapi/*': getTargetOptions()
proxy:{
'/web/grafana/*': getTargetOptions(),
'/web/config.*': getTargetOptions(),
'/pack/v1/*': getTargetOptions(),
'/portalapi/*': getTargetOptions(),
'/portal*': getTargetOptions(),
'/proxy/*': getTargetOptions(),
'/v1/*': getTargetOptions(),
'/app/*': getTargetOptions(),
'/sites/v1/*': getTargetOptions()
},
publicPath: ROOT +'/app',
hot: true,
https: true,
disableHostCheck: true,
https: true,
inline: true,
headers: { 'X-Custom-Header': 'yes' },
//stats: { colors: true },
stats: 'errors-only'
});
// tell webpack dev server to proxy below sockets requests to actual server
server.listeningApp.on('upgrade', function(req, socket) {
server.listeningApp.on('upgrade', function(req, socket) {
console.log('proxying ws', req.url);
proxy.ws(req, socket, {
target: 'wss://' + PROXY_TARGET,
secure: false
});
});
});
var htmlToSend = fs.readFileSync(__dirname + "//dist//index.html", 'utf8')
// to enable Hot Module Reload we need to serve local index.html.
// since local index.html has no embeded TOKEN, we need to:
// 1) make a proxy request
// 2) modify proxy response by replacing server index.html with the local
// 3) insert embeded by server token into the local
server.app.use(changeProxyResponse(
(req, res) => {
// return true if you want to modify the response later
var contentType = res.getHeader('Content-Type');
if (contentType && contentType.startsWith('text/html')) {
return true;
}
return false;
},
(req, res, body) => {
// body is a Buffer with the current response; return Buffer or string with the modified response
// can also return a Promise.
var str = body.toString();
htmlToSend = replaceToken(new RegExp(/<meta name="grv_csrf_token" .*\>/), str, htmlToSend);
htmlToSend = replaceToken(new RegExp(/<meta name="grv_bearer_token" .*\>/), str, htmlToSend);
return htmlToSend;
}
));
function replaceToken(regex, takeFrom, insertTo){
var value = takeFrom.match(regex);
if(value){
return insertTo.replace(regex, value[0]);
}
return insertTo;
}
server.app.use(ROOT, express.static(__dirname + "//dist"));
server.app.get(ROOT +'/*', function (req, res) {
res.sendfile(__dirname + "//dist//index.html");
proxy.web(req, res, getTargetOptions());
});
server.listen(PORT, "0.0.0.0", function() {
console.log('Dev Server is up and running: https://location:' + PORT +'/web');
console.log('Dev Server is up and running: https://location:' + PORT + '/web/');
});

114
web/devServerUtils.js Normal file
View file

@ -0,0 +1,114 @@
/*
Copyright 2015 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
//
// taken and modified from https://github.com/mo22/express-modify-response
//
module.exports = function expressModifyResponse(checkCallback, modifyCallback) {
return function expressModifyResponse(req, res, next) {
var _end = res.end;
var _write = res.write;
var checked = false;
var buffers = [];
var addBuffer = (chunk, encoding) => {
if (chunk === undefined)
return;
if (typeof chunk === 'string') {
chunk = new Buffer(chunk, encoding);
}
buffers.push(chunk);
};
var _writeHead = res.writeHead;
res.writeHead = function () {
// writeHead supports (statusCode, headers) as well as (statusCode,
// statusMessage, headers)
var headers = (arguments.length > 2)
? arguments[2]
: arguments[1];
var contentType = this.getHeader('content-type');
if ((typeof contentType != 'undefined') && (contentType.indexOf('text/html') == 0)) {
res.isHtml = true;
// Strip off the content length since it will change.
res.removeHeader('Content-Length');
if (headers) {
delete headers['content-length'];
}
}
_writeHead.apply(res, arguments);
};
res.write = function write(chunk, encoding) {
if (!checked) {
checked = true;
var hook = checkCallback(req, res);
if (!hook) {
res.end = _end;
res.write = _write;
return res.write(chunk, encoding);
} else {
addBuffer(chunk, encoding);
}
} else {
addBuffer(chunk, encoding);
}
};
res.end = function end(chunk, encoding) {
if (!checked) {
checked = true;
var hook = checkCallback(req, res);
if (!hook) {
res.end = _end;
res.write = _write;
return res.end(chunk, encoding);
} else {
addBuffer(chunk, encoding);
}
} else {
addBuffer(chunk, encoding);
}
var buffer = Buffer.concat(buffers);
Promise
.resolve(modifyCallback(req, res, buffer))
.then((result) => {
if (typeof result === 'string') {
result = new Buffer(result, 'utf-8');
}
if (res.getHeader('Content-Length')) {
res.setHeader('Content-Length', String(result.length));
}
res.end = _end;
res.write = _write;
res.write(result);
res.end();
})
.catch((e) => {
// handle?
next(e);
});
};
next();
}
}

View file

@ -2011,6 +2011,7 @@ webpackJsonp([0],[
contentType: 'application/json; charset=utf-8',
dataType: 'json',
beforeSend: function beforeSend(xhr) {
xhr.setRequestHeader('X-CSRF-Token', getXCSRFToken());
if (withToken) {
var bearerToken = _localStorage2.default.getBearerToken() || {};
var accessToken = bearerToken.accessToken;
@ -2045,6 +2046,11 @@ webpackJsonp([0],[
}
};
var getXCSRFToken = function getXCSRFToken() {
var metaTag = document.querySelector('[name=grv_csrf_token]');
return metaTag ? metaTag.content : '';
};
exports.default = api;
module.exports = exports['default'];
@ -2976,10 +2982,10 @@ webpackJsonp([0],[
return token;
},
_extractBearerTokenFromHtml: function _extractBearerTokenFromHtml() {
var el = document.querySelector("#bearer_token");
var el = document.querySelector('[name=grv_bearer_token]');
var token = null;
if (el !== null) {
var encodedToken = el.textContent || '';
var encodedToken = el.content || '';
if (encodedToken.length > EMPTY_TOKEN_CONTENT_LENGTH) {
var decoded = window.atob(encodedToken);
var json = JSON.parse(decoded);

View file

@ -1,4 +1,4 @@
!function(t){function e(n){if(r[n])return r[n].exports;var i=r[n]={exports:{},id:n,loaded:!1};return t[n].call(i.exports,i,i.exports,e),i.loaded=!0,i.exports}var n=window.webpackJsonp;window.webpackJsonp=function(o,a){for(var s,u,l=0,c=[];l<o.length;l++)u=o[l],i[u]&&c.push.apply(c,i[u]),i[u]=0;for(s in a)t[s]=a[s];for(n&&n(o,a);c.length;)c.shift().call(null,e);if(a[0])return r[0]=0,e(0)};var r={},i={2:0};return e.e=function(t,n){if(0===i[t])return n.call(null,e);if(void 0!==i[t])i[t].push(n);else{i[t]=[n];var r=document.getElementsByTagName("head")[0],o=document.createElement("script");o.type="text/javascript",o.charset="utf-8",o.async=!0,o.src=e.p+""+{0:"fc0baa6ecc1984e3945a",1:"19f124bc7fa95f2638fd"}[t]+".js",r.appendChild(o)}},e.m=t,e.c=r,e.p="/web/app",e(0)}([function(t,e,n){t.exports=n(560)},,function(t,e,n){"use strict";t.exports=n(3)},function(t,e,n){"use strict";var r=n(4),i=n(5),o=n(17),a=n(20),s=n(21),u=n(26),l=n(9),c=n(27),d=n(29),h=n(30),f=(n(11),l.createElement),p=l.createFactory,m=l.cloneElement,_=r,v={Children:{map:i.map,forEach:i.forEach,count:i.count,toArray:i.toArray,only:h},Component:o,PureComponent:a,createElement:f,cloneElement:m,isValidElement:l.isValidElement,PropTypes:c,createClass:s.createClass,createFactory:p,createMixin:function(t){return t},DOM:u,version:d,__spread:_};t.exports=v},function(t,e){/*
!function(t){function e(n){if(r[n])return r[n].exports;var i=r[n]={exports:{},id:n,loaded:!1};return t[n].call(i.exports,i,i.exports,e),i.loaded=!0,i.exports}var n=window.webpackJsonp;window.webpackJsonp=function(o,a){for(var s,u,l=0,c=[];l<o.length;l++)u=o[l],i[u]&&c.push.apply(c,i[u]),i[u]=0;for(s in a)t[s]=a[s];for(n&&n(o,a);c.length;)c.shift().call(null,e);if(a[0])return r[0]=0,e(0)};var r={},i={2:0};return e.e=function(t,n){if(0===i[t])return n.call(null,e);if(void 0!==i[t])i[t].push(n);else{i[t]=[n];var r=document.getElementsByTagName("head")[0],o=document.createElement("script");o.type="text/javascript",o.charset="utf-8",o.async=!0,o.src=e.p+""+{0:"660778fd716afb85cfa8",1:"19f124bc7fa95f2638fd"}[t]+".js",r.appendChild(o)}},e.m=t,e.c=r,e.p="/web/app",e(0)}([function(t,e,n){t.exports=n(560)},,function(t,e,n){"use strict";t.exports=n(3)},function(t,e,n){"use strict";var r=n(4),i=n(5),o=n(17),a=n(20),s=n(21),u=n(26),l=n(9),c=n(27),d=n(29),h=n(30),f=(n(11),l.createElement),p=l.createFactory,m=l.cloneElement,_=r,v={Children:{map:i.map,forEach:i.forEach,count:i.count,toArray:i.toArray,only:h},Component:o,PureComponent:a,createElement:f,cloneElement:m,isValidElement:l.isValidElement,PropTypes:c,createClass:s.createClass,createFactory:p,createMixin:function(t){return t},DOM:u,version:d,__spread:_};t.exports=v},function(t,e){/*
object-assign
(c) Sindre Sorhus
@license MIT

7
web/dist/index.html vendored
View file

@ -4,11 +4,12 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="grv_csrf_token" content="{{ .XCSRF }}" />
<meta name="grv_bearer_token" content="{{ .Session }}" />
<title>Teleport by Gravitational</title>
<script src="/web/config.js"></script>
<link rel="shortcut icon" href="/web/app/favicon.ico"><link href="/web/app/vendor.7c7e8bde8015ae63511442e683de6a37.css" rel="stylesheet"></head>
<body class="grv">
<div id="app"></div>
<div id="bearer_token" style="display: none;">{{.Session}}</div>
<script type="text/javascript" src="/web/app/vendor.b83772a8a4dadb99e5c4.js"></script><script type="text/javascript" src="/web/app/styles.b83772a8a4dadb99e5c4.js"></script><script type="text/javascript" src="/web/app/app.b83772a8a4dadb99e5c4.js"></script></body>
<div id="app"></div>
<script type="text/javascript" src="/web/app/vendor.d0f1230171ff8c0e4540.js"></script><script type="text/javascript" src="/web/app/styles.d0f1230171ff8c0e4540.js"></script><script type="text/javascript" src="/web/app/app.d0f1230171ff8c0e4540.js"></script></body>
</html>

View file

@ -41,13 +41,14 @@ const api = {
type: 'GET',
contentType: 'application/json; charset=utf-8',
dataType: 'json',
beforeSend: function(xhr) {
beforeSend: function (xhr) {
xhr.setRequestHeader('X-CSRF-Token', getXCSRFToken());
if (withToken) {
const bearerToken = localStorage.getBearerToken() || {};
const { accessToken } = bearerToken;
xhr.setRequestHeader('Authorization','Bearer ' + accessToken);
xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
}
}
}
}
return $.ajax($.extend({}, defaultCfg, cfg));
@ -76,4 +77,9 @@ const api = {
}
}
const getXCSRFToken = () => {
const metaTag = document.querySelector('[name=grv_csrf_token]');
return metaTag ? metaTag.content : ''
}
export default api;

View file

@ -87,10 +87,10 @@ const session = {
},
_extractBearerTokenFromHtml() {
const el = document.querySelector("#bearer_token")
const el = document.querySelector('[name=grv_bearer_token]');
let token = null;
if (el !== null) {
let encodedToken = el.textContent || '';
let encodedToken = el.content || '';
if (encodedToken.length > EMPTY_TOKEN_CONTENT_LENGTH) {
let decoded = window.atob(encodedToken);
let json = JSON.parse(decoded);

View file

@ -4,11 +4,12 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="grv_csrf_token" content="{{ .XCSRF }}" />
<meta name="grv_bearer_token" content="{{ .Session }}" />
<title><%= htmlWebpackPlugin.options.title %></title>
<script src="/web/config.js"></script>
</head>
<body class="grv">
<div id="app"></div>
<div id="bearer_token" style="display: none;">{{.Session}}</div>
<div id="app"></div>
</body>
</html>