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  }