github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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 "net/http" 8 9 "github.com/juju/errors" 10 "github.com/juju/httprequest" 11 "github.com/juju/loggo" 12 "github.com/julienschmidt/httprouter" 13 "gopkg.in/juju/names.v2" 14 "gopkg.in/macaroon-bakery.v2-unstable/bakery" 15 "gopkg.in/macaroon-bakery.v2-unstable/bakery/checkers" 16 "gopkg.in/macaroon-bakery.v2-unstable/httpbakery" 17 macaroon "gopkg.in/macaroon.v2-unstable" 18 19 "github.com/juju/juju/apiserver/apiserverhttp" 20 "github.com/juju/juju/apiserver/authentication" 21 "github.com/juju/juju/apiserver/params" 22 "github.com/juju/juju/state" 23 ) 24 25 var ( 26 logger = loggo.GetLogger("juju.apiserver.stateauthenticator") 27 ) 28 29 type localLoginHandlers struct { 30 authCtxt *authContext 31 finder state.EntityFinder 32 } 33 34 // AddHandlers adds the local login handlers to the given mux. 35 func (h *localLoginHandlers) AddHandlers(mux *apiserverhttp.Mux) { 36 var errorMapper httprequest.ErrorMapper = httpbakery.ErrorToResponse 37 var handleJSON = errorMapper.HandleJSON 38 makeHandler := func(h httprouter.Handle) http.Handler { 39 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 40 h(w, req, nil) 41 }) 42 } 43 dischargeMux := http.NewServeMux() 44 httpbakery.AddDischargeHandler( 45 dischargeMux, 46 localUserIdentityLocationPath, 47 h.authCtxt.localUserThirdPartyBakeryService, 48 h.checkThirdPartyCaveat, 49 ) 50 dischargeMux.Handle( 51 localUserIdentityLocationPath+"/login", 52 makeHandler(handleJSON(h.serveLogin)), 53 ) 54 dischargeMux.Handle( 55 localUserIdentityLocationPath+"/wait", 56 makeHandler(handleJSON(h.serveWait)), 57 ) 58 mux.AddHandler("POST", localUserIdentityLocationPath+"/discharge", dischargeMux) 59 mux.AddHandler("GET", localUserIdentityLocationPath+"/publickey", dischargeMux) 60 mux.AddHandler("GET", localUserIdentityLocationPath+"/wait", dischargeMux) 61 mux.AddHandler("GET", localUserIdentityLocationPath+"/login", dischargeMux) 62 mux.AddHandler("POST", localUserIdentityLocationPath+"/login", dischargeMux) 63 } 64 65 func (h *localLoginHandlers) serveLogin(p httprequest.Params) (interface{}, error) { 66 switch p.Request.Method { 67 case "POST": 68 return h.serveLoginPost(p) 69 case "GET": 70 return h.serveLoginGet(p) 71 default: 72 return nil, errors.Errorf("unsupported method %q", p.Request.Method) 73 } 74 } 75 76 func (h *localLoginHandlers) serveLoginPost(p httprequest.Params) (interface{}, error) { 77 if err := p.Request.ParseForm(); err != nil { 78 return nil, err 79 } 80 waitId := p.Request.Form.Get("waitid") 81 if waitId == "" { 82 return nil, errors.NotValidf("missing waitid") 83 } 84 username := p.Request.Form.Get("user") 85 password := p.Request.Form.Get("password") 86 if !names.IsValidUser(username) { 87 return nil, errors.NotValidf("username %q", username) 88 } 89 userTag := names.NewUserTag(username) 90 if !userTag.IsLocal() { 91 return nil, errors.NotValidf("non-local username %q", username) 92 } 93 94 authenticator := h.authCtxt.authenticator(p.Request.Host) 95 if _, err := authenticator.Authenticate(h.finder, userTag, params.LoginRequest{ 96 Credentials: password, 97 }); err != nil { 98 // Mark the interaction as done (but failed), 99 // unblocking a pending "/auth/wait" request. 100 if err := h.authCtxt.localUserInteractions.Done(waitId, userTag, err); err != nil { 101 if !errors.IsNotFound(err) { 102 logger.Warningf( 103 "failed to record completion of interaction %q for %q", 104 waitId, userTag.Id(), 105 ) 106 } 107 } 108 return nil, errors.Trace(err) 109 } 110 111 // Provide the client with a macaroon that they can use to 112 // prove that they have logged in, and obtain a discharge 113 // macaroon. 114 m, err := h.authCtxt.CreateLocalLoginMacaroon(userTag) 115 if err != nil { 116 return nil, err 117 } 118 cookie, err := httpbakery.NewCookie(macaroon.Slice{m}) 119 if err != nil { 120 return nil, err 121 } 122 http.SetCookie(p.Response, cookie) 123 124 // Mark the interaction as done, unblocking a pending 125 // "/auth/wait" request. 126 if err := h.authCtxt.localUserInteractions.Done( 127 waitId, userTag, nil, 128 ); err != nil { 129 if errors.IsNotFound(err) { 130 err = errors.New("login timed out") 131 } 132 return nil, err 133 } 134 return nil, nil 135 } 136 137 func (h *localLoginHandlers) serveLoginGet(p httprequest.Params) (interface{}, error) { 138 if p.Request.Header.Get("Accept") == "application/json" { 139 // The application/json content-type is used to 140 // inform the client of the supported auth methods. 141 return map[string]string{ 142 "juju_userpass": p.Request.URL.String(), 143 }, nil 144 } 145 // TODO(axw) return an HTML form. If waitid is supplied, 146 // it should be passed through so we can unblock a request 147 // on the /auth/wait endpoint. We should also support logging 148 // in when not specifically directed to the login page. 149 return nil, errors.NotImplementedf("GET") 150 } 151 152 func (h *localLoginHandlers) serveWait(p httprequest.Params) (interface{}, error) { 153 if err := p.Request.ParseForm(); err != nil { 154 return nil, err 155 } 156 if p.Request.Method != "GET" { 157 return nil, errors.Errorf("unsupported method %q", p.Request.Method) 158 } 159 waitId := p.Request.Form.Get("waitid") 160 if waitId == "" { 161 return nil, errors.NotValidf("missing waitid") 162 } 163 interaction, err := h.authCtxt.localUserInteractions.Wait(waitId, nil) 164 if err != nil { 165 return nil, errors.Trace(err) 166 } 167 if interaction.LoginError != nil { 168 return nil, errors.Trace(err) 169 } 170 ctx := macaroonAuthContext{ 171 authContext: h.authCtxt, 172 req: p.Request, 173 } 174 macaroon, err := h.authCtxt.localUserThirdPartyBakeryService.Discharge( 175 &ctx, interaction.CaveatId, 176 ) 177 if err != nil { 178 return nil, errors.Annotate(err, "discharging macaroon") 179 } 180 return httpbakery.WaitResponse{macaroon}, nil 181 } 182 183 func (h *localLoginHandlers) checkThirdPartyCaveat(req *http.Request, cavInfo *bakery.ThirdPartyCaveatInfo) ([]checkers.Caveat, error) { 184 ctx := &macaroonAuthContext{authContext: h.authCtxt, req: req} 185 return ctx.CheckThirdPartyCaveat(cavInfo) 186 } 187 188 type macaroonAuthContext struct { 189 *authContext 190 req *http.Request 191 } 192 193 // CheckThirdPartyCaveat is part of the bakery.ThirdPartyChecker interface. 194 func (ctx *macaroonAuthContext) CheckThirdPartyCaveat(cavInfo *bakery.ThirdPartyCaveatInfo) ([]checkers.Caveat, error) { 195 tag, err := ctx.CheckLocalLoginCaveat(cavInfo.Condition) 196 if err != nil { 197 return nil, errors.Trace(err) 198 } 199 firstPartyCaveats, err := ctx.CheckLocalLoginRequest(ctx.req, tag) 200 if err != nil { 201 if _, ok := errors.Cause(err).(*bakery.VerificationError); ok { 202 waitId, err := ctx.localUserInteractions.Start( 203 cavInfo.CaveatId, 204 ctx.clock.Now().Add(authentication.LocalLoginInteractionTimeout), 205 ) 206 if err != nil { 207 return nil, errors.Trace(err) 208 } 209 visitURL := localUserIdentityLocationPath + "/login?waitid=" + waitId 210 waitURL := localUserIdentityLocationPath + "/wait?waitid=" + waitId 211 return nil, httpbakery.NewInteractionRequiredError(visitURL, waitURL, nil, ctx.req) 212 } 213 return nil, errors.Trace(err) 214 } 215 return firstPartyCaveats, nil 216 }