github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/stateauthenticator/locallogin.go (about) 1 // Copyright 2016-2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package stateauthenticator 5 6 import ( 7 "context" 8 "crypto/rand" 9 "fmt" 10 "net/http" 11 12 "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 13 "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 14 "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" 15 "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/form" 16 "github.com/juju/errors" 17 "github.com/juju/loggo" 18 "github.com/juju/names/v5" 19 "gopkg.in/httprequest.v1" 20 21 "github.com/juju/juju/apiserver/apiserverhttp" 22 "github.com/juju/juju/apiserver/authentication" 23 "github.com/juju/juju/state" 24 ) 25 26 type localLoginHandlers struct { 27 authCtxt *authContext 28 finder state.EntityFinder 29 userTokens map[string]string 30 } 31 32 const ( 33 formURL = "/form" 34 ) 35 36 var ( 37 logger = loggo.GetLogger("juju.apiserver.stateauthenticator") 38 ) 39 40 // AddHandlers adds the local login handlers to the given mux. 41 func (h *localLoginHandlers) AddHandlers(mux *apiserverhttp.Mux) { 42 dischargeMux := http.NewServeMux() 43 discharger := httpbakery.NewDischarger( 44 httpbakery.DischargerParams{ 45 Key: h.authCtxt.localUserThirdPartyBakeryKey, 46 Checker: httpbakery.ThirdPartyCaveatCheckerFunc(h.checkThirdPartyCaveat), 47 }) 48 discharger.AddMuxHandlers(dischargeMux, localUserIdentityLocationPath) 49 50 dischargeMux.Handle( 51 localUserIdentityLocationPath+formURL, 52 http.HandlerFunc(h.formHandler), 53 ) 54 _ = mux.AddHandler("POST", localUserIdentityLocationPath+"/discharge", dischargeMux) 55 _ = mux.AddHandler("GET", localUserIdentityLocationPath+"/publickey", dischargeMux) 56 _ = mux.AddHandler("GET", localUserIdentityLocationPath+"/form", dischargeMux) 57 _ = mux.AddHandler("POST", localUserIdentityLocationPath+"/form", dischargeMux) 58 } 59 60 func (h *localLoginHandlers) bakeryError(w http.ResponseWriter, err error) { 61 httpbakery.WriteError(context.TODO(), w, err) 62 } 63 64 func (h *localLoginHandlers) formHandler(w http.ResponseWriter, req *http.Request) { 65 if req.Method != http.MethodPost { 66 http.Error(w, fmt.Sprintf("%s method not allowed", req.Method), http.StatusMethodNotAllowed) 67 return 68 } 69 70 ctx := req.Context() 71 reqParams := httprequest.Params{ 72 Response: w, 73 Request: req, 74 Context: ctx, 75 } 76 loginRequest := form.LoginRequest{} 77 if err := httprequest.Unmarshal(reqParams, &loginRequest); err != nil { 78 h.bakeryError(w, errors.Annotate(err, "can't unmarshal login request")) 79 return 80 } 81 82 username := loginRequest.Body.Form["user"].(string) 83 password := loginRequest.Body.Form["password"].(string) 84 userTag := names.NewUserTag(username) 85 if !userTag.IsLocal() { 86 h.bakeryError(w, errors.NotValidf("non-local username %q", username)) 87 return 88 } 89 90 authenticator := h.authCtxt.authenticator(req.Host) 91 if _, err := authenticator.Authenticate(ctx, h.finder, authentication.AuthParams{ 92 AuthTag: userTag, 93 Credentials: password, 94 }); err != nil { 95 h.bakeryError(w, err) 96 return 97 } 98 99 token, err := newID() 100 if err != nil { 101 h.bakeryError(w, errors.Annotate(err, "cannot generate token")) 102 return 103 } 104 h.userTokens[token] = username 105 106 loginResponse := form.LoginResponse{ 107 Token: &httpbakery.DischargeToken{ 108 Kind: "juju_userpass", 109 Value: []byte(token), 110 }, 111 } 112 _ = httprequest.WriteJSON(w, http.StatusOK, loginResponse) 113 } 114 115 func newID() (string, error) { 116 var id [12]byte 117 if _, err := rand.Read(id[:]); err != nil { 118 return "", fmt.Errorf("cannot read random id: %v", err) 119 } 120 return fmt.Sprintf("%x", id[:]), nil 121 } 122 123 func (h *localLoginHandlers) checkThirdPartyCaveat(stdCtx context.Context, req *http.Request, cavInfo *bakery.ThirdPartyCaveatInfo, token *httpbakery.DischargeToken) ([]checkers.Caveat, error) { 124 tag, err := h.authCtxt.CheckLocalLoginCaveat(string(cavInfo.Condition)) 125 if err != nil { 126 return nil, errors.Trace(err) 127 } 128 if token == nil { 129 if err := h.authCtxt.CheckLocalLoginRequest(stdCtx, req); err == nil { 130 return h.authCtxt.DischargeCaveats(tag), nil 131 } 132 err2 := httpbakery.NewInteractionRequiredError(nil, req) 133 err2.SetInteraction("juju_userpass", form.InteractionInfo{URL: localUserIdentityLocationPath + formURL}) 134 return nil, err2 135 } 136 137 tokenString := string(token.Value) 138 username, ok := h.userTokens[tokenString] 139 delete(h.userTokens, tokenString) 140 if token.Kind != "juju_userpass" || !ok { 141 return nil, errors.Errorf("invalid token %#v", token) 142 } 143 144 // Sanity check. 145 if tag.Id() != username { 146 return nil, errors.Errorf("discharge token for user %q does not match declared user %q", username, tag.Id()) 147 } 148 return h.authCtxt.DischargeCaveats(tag), nil 149 }