go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/internal/auth.go (about) 1 // Copyright 2020 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 internal 16 17 import ( 18 "fmt" 19 "net/http" 20 21 "go.chromium.org/luci/auth/identity" 22 "go.chromium.org/luci/common/errors" 23 "go.chromium.org/luci/common/logging" 24 25 "go.chromium.org/luci/server/auth" 26 "go.chromium.org/luci/server/auth/openid" 27 "go.chromium.org/luci/server/router" 28 ) 29 30 // CloudAuthMiddleware returns a middleware chain that authorizes requests from 31 // Cloud Tasks and Cloud Scheduler. 32 // 33 // Checks OpenID Connect tokens have us in the audience, and the email in them 34 // is in `callers` list. 35 // 36 // If `header` is set, will also accept requests that have this header, 37 // regardless of its value. This is used to authorize GAE tasks and crons based 38 // on `X-AppEngine-*` headers. 39 func CloudAuthMiddleware(callers []string, header string, rejected func(*router.Context)) router.MiddlewareChain { 40 oidc := auth.Authenticate(&openid.GoogleIDTokenAuthMethod{ 41 AudienceCheck: openid.AudienceMatchesHost, 42 }) 43 44 return router.NewMiddlewareChain(oidc, func(c *router.Context, next router.Handler) { 45 if header != "" && c.Request.Header.Get(header) != "" { 46 next(c) 47 return 48 } 49 50 if ident := auth.CurrentIdentity(c.Request.Context()); ident.Kind() != identity.Anonymous { 51 if checkContainsIdent(callers, ident) { 52 next(c) 53 } else { 54 if rejected != nil { 55 rejected(c) 56 } 57 httpReply(c, 403, 58 fmt.Sprintf("Caller %q is not authorized", ident), 59 errors.Reason("expecting any of %q", callers).Err(), 60 ) 61 } 62 return 63 } 64 65 var err error 66 if header != "" { 67 err = errors.Reason("no OIDC token and no %s header", header).Err() 68 } else { 69 err = errors.Reason("no OIDC token").Err() 70 } 71 if rejected != nil { 72 rejected(c) 73 } 74 httpReply(c, 403, "Authentication required", err) 75 }) 76 } 77 78 // checkContainsIdent is true if `ident` email matches some of `callers`. 79 func checkContainsIdent(callers []string, ident identity.Identity) bool { 80 if ident.Kind() != identity.User { 81 return false // we want service accounts 82 } 83 email := ident.Email() 84 for _, c := range callers { 85 if email == c { 86 return true 87 } 88 } 89 return false 90 } 91 92 // httpReply writes and logs HTTP response. 93 // 94 // `msg` is sent to the caller as is. `err` is logged, but not sent. 95 func httpReply(c *router.Context, code int, msg string, err error) { 96 if err != nil { 97 logging.Errorf(c.Request.Context(), "%s: %s", msg, err) 98 } 99 http.Error(c.Writer, msg, code) 100 }