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  }