go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/xsrf/xsrf.go (about)

     1  // Copyright 2015 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package xsrf provides Cross Site Request Forgery prevention middleware.
    16  //
    17  // Usage:
    18  //  1. When serving GET request put hidden "xsrf_token" input field with
    19  //     the token value into the form. Use TokenField(...) to generate it.
    20  //  2. Wrap POST-handling route with WithTokenCheck(...) middleware.
    21  package xsrf
    22  
    23  import (
    24  	"context"
    25  	"fmt"
    26  	"html"
    27  	"html/template"
    28  	"net/http"
    29  	"time"
    30  
    31  	"go.chromium.org/luci/common/logging"
    32  	"go.chromium.org/luci/common/retry/transient"
    33  
    34  	"go.chromium.org/luci/server/auth"
    35  	"go.chromium.org/luci/server/router"
    36  	"go.chromium.org/luci/server/tokens"
    37  )
    38  
    39  // xsrfToken described how to generate tokens.
    40  var xsrfToken = tokens.TokenKind{
    41  	Algo:       tokens.TokenAlgoHmacSHA256,
    42  	Expiration: 4 * time.Hour,
    43  	SecretKey:  "xsrf_token",
    44  	Version:    1,
    45  }
    46  
    47  // Token generates new XSRF token bound to the current caller.
    48  //
    49  // The token is URL safe base64 encoded string. It lives for 4 hours and may
    50  // potentially be used multiple times (i.e. the token is stateless).
    51  //
    52  // Put it in hidden form field under the name of "xsrf_token", e.g.
    53  // <input type="hidden" name="xsrf_token" value="{{.XsrfToken}}">.
    54  //
    55  // Later WithTokenCheck will grab it from there and verify its validity.
    56  func Token(ctx context.Context) (string, error) {
    57  	return xsrfToken.Generate(ctx, state(ctx), nil, 0)
    58  }
    59  
    60  // Check returns nil if XSRF token is valid.
    61  func Check(ctx context.Context, tok string) error {
    62  	_, err := xsrfToken.Validate(ctx, tok, state(ctx))
    63  	return err
    64  }
    65  
    66  // TokenField generates "<input type="hidden" ...>" field with the token.
    67  //
    68  // It can be put into HTML forms directly. Panics on errors.
    69  func TokenField(ctx context.Context) template.HTML {
    70  	tok, err := Token(ctx)
    71  	if err != nil {
    72  		panic(err)
    73  	}
    74  	return template.HTML(fmt.Sprintf(`<input type="hidden" name="xsrf_token" value="%s">`, html.EscapeString(tok)))
    75  }
    76  
    77  // WithTokenCheck is middleware that checks validity of XSRF tokens.
    78  //
    79  // If searches for the token in "xsrf_token" POST form field (as generated by
    80  // TokenField). Aborts the request with HTTP 403 if XSRF token is missing or
    81  // invalid.
    82  func WithTokenCheck(c *router.Context, next router.Handler) {
    83  	tok := c.Request.PostFormValue("xsrf_token")
    84  	if tok == "" {
    85  		replyError(c.Request.Context(), c.Writer, http.StatusForbidden, "XSRF token is missing")
    86  		return
    87  	}
    88  	switch err := Check(c.Request.Context(), tok); {
    89  	case transient.Tag.In(err):
    90  		replyError(c.Request.Context(), c.Writer, http.StatusInternalServerError, "Transient error when checking XSRF token - %s", err)
    91  	case err != nil:
    92  		replyError(c.Request.Context(), c.Writer, http.StatusForbidden, "Bad XSRF token - %s", err)
    93  	default:
    94  		next(c)
    95  	}
    96  }
    97  
    98  ///
    99  
   100  // state must return exact same value when generating and verifying token for
   101  // the verification to succeed.
   102  func state(ctx context.Context) []byte {
   103  	return []byte(auth.CurrentUser(ctx).Identity)
   104  }
   105  
   106  // replyError sends error response and logs it.
   107  func replyError(ctx context.Context, rw http.ResponseWriter, code int, msg string, args ...any) {
   108  	text := fmt.Sprintf(msg, args...)
   109  	logging.Errorf(ctx, "xsrf: %s", text)
   110  	http.Error(rw, text, code)
   111  }