mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-31 20:39:37 +08:00
* feat: set cluster connector name on first user signin * perf: move is passwordless enabled before get users * fix: move logic to correct handler * test: test setting default passwordless connector name for cloud (positive case) * test: add negative test cases * refactor: flatten nested code * fix: remove return in case auth preference was not set * fix: run logic only in cloud * docs: typo * fix: run when passwordless * test: move trySettingConnectorNameToPasswordless as separete method * docs: add safety comments * test: rephrase safety comment * refactor: remove space between call and error check * fix: remove unnessesery error log from trySettingConnectorNameToPasswordless * refactor: pass context, rename session context * docs: add description to trySettingConnectorNameToPasswordless * test: add descriptive assertion text Co-authored-by: Alan Parra <alan.parra@goteleport.com> * refactor: simplify error handilng Co-authored-by: Alan Parra <alan.parra@goteleport.com> * fix: check for non default setting Co-authored-by: Alan Parra <alan.parra@goteleport.com> * test: correct RPID * refactor: simplify user creation Co-authored-by: Alan Parra <alan.parra@goteleport.com> * docs: fix typo Co-authored-by: Alan Parra <alan.parra@goteleport.com> * test: reorder name filed in the test cases struct * test: better names for test cases * refactor: generate token instead of hardcoding arbitrary value * test: remove user agent header * fix: check for empty password * docs: add passwordless check description Co-authored-by: Alan Parra <alan.parra@goteleport.com> * test: add "first cloud sign-in does not change custom connector" * test: use correct number of users in "first cloud sign-in does not change custom connector" Co-authored-by: Alan Parra <alan.parra@goteleport.com> * test: remove is cloud check from test helpers, instead use config * refactor: store is_passwordless_registration in variable * test: first cloud sign-in with password does not change connector * refactor: change isPasswordlessRegistration name Co-authored-by: Alan Parra <alan.parra@goteleport.com>
151 lines
4.2 KiB
Go
151 lines
4.2 KiB
Go
/*
|
|
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/trace"
|
|
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
)
|
|
|
|
const (
|
|
// CookieName is the name of the CSRF cookie. It's prefixed with "__Host-" as
|
|
// an additional defense in depth measure. It makes sure it is sent from a
|
|
// secure page (HTTPS), won't be sent to subdomains, and the path attribute
|
|
// is set to /.
|
|
CookieName = "__Host-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
|
|
)
|
|
|
|
// GenerateToken generates a random CSRF token.
|
|
func GenerateToken() (string, error) {
|
|
return utils.CryptoRandomHex(tokenLenBytes)
|
|
}
|
|
|
|
// 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) {
|
|
token, err := ExtractTokenFromCookie(r)
|
|
// if there was an error retrieving the token, the token doesn't exist
|
|
if err != nil || len(token) == 0 {
|
|
token, err = GenerateToken()
|
|
if err != nil {
|
|
return "", trace.Wrap(err)
|
|
}
|
|
}
|
|
save(token, w)
|
|
return token, nil
|
|
}
|
|
|
|
// VerifyHTTPHeader checks if HTTP header value matches the cookie.
|
|
func VerifyHTTPHeader(r *http.Request) error {
|
|
token := r.Header.Get(HeaderName)
|
|
if len(token) == 0 {
|
|
return trace.BadParameter("cannot retrieve CSRF token from HTTP header %q", HeaderName)
|
|
}
|
|
|
|
err := VerifyToken(token, r)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// VerifyToken validates given token based on HTTP request cookie
|
|
func VerifyToken(token string, r *http.Request) error {
|
|
realToken, err := ExtractTokenFromCookie(r)
|
|
if err != nil {
|
|
return trace.Wrap(err, "unable to extract CSRF token from cookie")
|
|
}
|
|
|
|
decodedTokenA, err := decode(token)
|
|
if err != nil {
|
|
return trace.Wrap(err, "unable to decode CSRF token")
|
|
}
|
|
|
|
decodedTokenB, err := decode(realToken)
|
|
if err != nil {
|
|
return trace.Wrap(err, "unable to decode cookie CSRF token")
|
|
}
|
|
|
|
if !compareTokens(decodedTokenA, decodedTokenB) {
|
|
return trace.BadParameter("CSRF tokens do not match")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ExtractTokenFromCookie retrieves a CSRF token from the session cookie.
|
|
func ExtractTokenFromCookie(r *http.Request) (string, error) {
|
|
cookie, err := r.Cookie(CookieName)
|
|
if err != nil {
|
|
return "", trace.Wrap(err)
|
|
}
|
|
|
|
return cookie.Value, nil
|
|
}
|
|
|
|
// decode decodes a cookie using base64.
|
|
func decode(token string) ([]byte, error) {
|
|
decoded, err := hex.DecodeString(token)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
if len(decoded) != tokenLenBytes {
|
|
return nil, trace.BadParameter("invalid CSRF token byte length, expected %v, got %v", tokenLenBytes, len(decoded))
|
|
}
|
|
|
|
return decoded, nil
|
|
}
|
|
|
|
// compareTokens securely (constant-time) compares CSRF tokens
|
|
func compareTokens(a, b []byte) bool {
|
|
return subtle.ConstantTimeCompare(a, b) == 1
|
|
}
|
|
|
|
// 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,
|
|
// Set SameSite to none so browsers preserve gravitational CSRF cookie
|
|
// while processing SSO providers redirects.
|
|
SameSite: http.SameSiteNoneMode,
|
|
HttpOnly: true,
|
|
Secure: true,
|
|
Path: "/",
|
|
}
|
|
|
|
// write the authenticated cookie to the response.
|
|
http.SetCookie(w, cookie)
|
|
w.Header().Add("Vary", "Cookie")
|
|
return encodedToken
|
|
}
|