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 }