Files
teleport/lib/httplib/csrf/csrf.go
T
jkaczmarkiewiczandAlan Parra 2a9167b03b set cluster connector name on signin for first cloud user (#17834)
* 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>
2022-11-14 14:52:42 +00:00

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
}