github.com/henvic/wedeploycli@v1.7.6-0.20200319005353-3630f582f284/loginserver/loginserver.go (about)

     1  package loginserver
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net"
     8  	"net/http"
     9  	"net/url"
    10  	"strings"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/hashicorp/errwrap"
    15  	"github.com/henvic/wedeploycli/apihelper"
    16  	"github.com/henvic/wedeploycli/config"
    17  	"github.com/henvic/wedeploycli/defaults"
    18  	"github.com/henvic/wedeploycli/usertoken"
    19  	"github.com/henvic/wedeploycli/verbose"
    20  )
    21  
    22  // Service server for receiving JSON Web Token
    23  type Service struct {
    24  	Infrastructure string
    25  	ctx            context.Context
    26  	ctxCancel      context.CancelFunc
    27  	netListener    net.Listener
    28  	httpServer     *http.Server
    29  	serverAddress  string
    30  	temporaryToken string
    31  	jwt            usertoken.JSONWebToken
    32  	err            error
    33  }
    34  
    35  // BUG(henvic): Ajax could be used to avoid a small risk of the user being stuck in a white error
    36  // when the authentication fails (at some implementation cost).
    37  const redirectPage = `<html>
    38  <body>
    39  <form action="/authenticate" method="post" id="authenticate">
    40  <input type="hidden" id="access_token" name="access_token" />
    41  </form>
    42  <noscript>
    43  You need JavaScript enabled to complete the authentication. Enable it and try again.
    44  </noscript>
    45  <script>
    46  var accessToken = document.location.hash.replace('#access_token=', '');
    47  var rm = "#access_token=";
    48  
    49  if (accessToken.indexOf(rm) === 0) {
    50  	accessToken = accessToken.substr(rm.length)
    51  }
    52  
    53  document.querySelector("#access_token").value = accessToken;
    54  document.querySelector("#authenticate").submit();
    55  </script>
    56  </body>
    57  </html>`
    58  
    59  // Listen for requests
    60  func (s *Service) Listen(ctx context.Context) (address string, err error) {
    61  	s.ctx, s.ctxCancel = context.WithTimeout(ctx, 15*time.Minute)
    62  	s.netListener, err = net.Listen("tcp", "127.0.0.1:0")
    63  
    64  	if err != nil {
    65  		return "", errwrap.Wrapf("can't start authentication service: {{err}}", err)
    66  	}
    67  
    68  	s.serverAddress = fmt.Sprintf("http://localhost:%v",
    69  		strings.TrimPrefix(
    70  			s.netListener.Addr().String(),
    71  			"127.0.0.1:"))
    72  
    73  	return s.serverAddress, nil
    74  }
    75  
    76  func (s *Service) waitServer(w *sync.WaitGroup) {
    77  	<-s.ctx.Done()
    78  	var err = s.httpServer.Shutdown(s.ctx)
    79  	if err != nil && err != context.Canceled {
    80  		s.err = errwrap.Wrapf("can't shutdown login service properly: {{err}}", err)
    81  	}
    82  	w.Done()
    83  }
    84  
    85  // Serve HTTP requests
    86  func (s *Service) Serve() error {
    87  	if s.netListener == nil {
    88  		return errors.New("server is not open yet")
    89  	}
    90  
    91  	s.httpServer = &http.Server{
    92  		Handler: &handler{
    93  			handler: s.httpHandler,
    94  		},
    95  	}
    96  
    97  	var w sync.WaitGroup
    98  	w.Add(1)
    99  	go s.waitServer(&w)
   100  
   101  	var serverErr = s.httpServer.Serve(s.netListener)
   102  
   103  	if serverErr != http.ErrServerClosed {
   104  		verbose.Debug(fmt.Sprintf("Error closing authentication server: %v", serverErr))
   105  	}
   106  
   107  	w.Wait()
   108  	return s.err
   109  }
   110  
   111  func (s *Service) redirectToDashboard(w http.ResponseWriter, r *http.Request) {
   112  	var page string
   113  
   114  	switch s.err {
   115  	case nil:
   116  		page = "static/cli/login-success/"
   117  	case ErrSignUpEmailConfirmation:
   118  		page = "static/cli/login-requires-email-confirmation/"
   119  	default:
   120  		page = "static/cli/login-failure/"
   121  	}
   122  
   123  	var redirect = fmt.Sprintf("https://%v%v/%v", defaults.DashboardAddressPrefix, s.Infrastructure, page)
   124  	http.Redirect(w, r, redirect, http.StatusSeeOther)
   125  }
   126  
   127  func (s *Service) httpHandler(w http.ResponseWriter, r *http.Request) {
   128  	switch r.URL.Path {
   129  	case "/":
   130  		s.homeHandler(w, r)
   131  	case "/authenticate":
   132  		s.authenticateHandler(w, r)
   133  	default:
   134  		http.NotFound(w, r)
   135  	}
   136  }
   137  
   138  const safeErrorPageTemplate = `<html>
   139  <body>
   140  <script>
   141  document.location.hash = ""
   142  </script>
   143  %s
   144  </body>
   145  </html>
   146  `
   147  
   148  // safeErrorHandler basically clears any access_token from the fragment
   149  // and does what http.Error does
   150  func safeErrorHandler(w http.ResponseWriter, body string, code int) {
   151  	w.Header().Set("Content-Type", "text/html; charset=utf-8")
   152  	w.Header().Set("X-Content-Type-Options", "nosniff")
   153  
   154  	w.WriteHeader(code)
   155  	_, _ = fmt.Fprintf(w, safeErrorPageTemplate, body)
   156  }
   157  
   158  func (s *Service) homeHandler(w http.ResponseWriter, r *http.Request) {
   159  	referer, _ := url.Parse(r.Header.Get("Referer"))
   160  
   161  	// this is a compromise
   162  	var dashboard = defaults.DashboardAddressPrefix + s.Infrastructure
   163  	if referer.Host != "" && referer.Host != dashboard {
   164  		s.err = errors.New("token origin is not from given dashboard")
   165  		safeErrorHandler(w, "403 Forbidden", http.StatusForbidden)
   166  		s.ctxCancel()
   167  		return
   168  	}
   169  
   170  	_, _ = fmt.Fprintln(w, redirectPage)
   171  }
   172  
   173  // ErrSignUpEmailConfirmation tells that sign up was canceled because user is signing up
   174  var ErrSignUpEmailConfirmation = errors.New(`sign up on Liferay Cloud requested: try "lcp login" once you confirm your email`)
   175  
   176  const signupRequestPseudoToken = "signup_requested"
   177  
   178  func (s *Service) authenticateHandler(w http.ResponseWriter, r *http.Request) {
   179  	if r.Method != http.MethodPost || r.Header.Get("Referer") != s.serverAddress+"/" {
   180  		s.err = errors.New("authentication should have been POSTed and from a localhost origin")
   181  		safeErrorHandler(w, "403 Forbidden", http.StatusForbidden)
   182  		s.ctxCancel()
   183  		return
   184  	}
   185  
   186  	var pferr = r.ParseForm()
   187  
   188  	if pferr != nil {
   189  		s.err = errwrap.Wrapf("can't parse authentication form: {{err}}", pferr)
   190  		safeErrorHandler(w, "400 Bad Request", http.StatusBadRequest)
   191  		s.ctxCancel()
   192  		return
   193  	}
   194  
   195  	s.temporaryToken = r.Form.Get("access_token")
   196  	verbose.Debug("Access Token: " + verbose.SafeEscape(s.temporaryToken))
   197  
   198  	switch s.temporaryToken {
   199  	case signupRequestPseudoToken:
   200  		s.err = ErrSignUpEmailConfirmation
   201  	default:
   202  		s.jwt, s.err = usertoken.ParseUnsignedJSONWebToken(s.temporaryToken)
   203  	}
   204  
   205  	s.redirectToDashboard(w, r)
   206  	s.ctxCancel()
   207  }
   208  
   209  // Credentials for authenticated user or error, it blocks until the information is available
   210  func (s *Service) Credentials() (username string, token string, err error) {
   211  	<-s.ctx.Done()
   212  	return s.jwt.Email, s.temporaryToken, s.err
   213  }
   214  
   215  type handler struct {
   216  	handler func(w http.ResponseWriter, r *http.Request)
   217  }
   218  
   219  func (s *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   220  	s.handler(w, r)
   221  }
   222  
   223  type accessToken struct {
   224  	AccessToken string `json:"token"`
   225  }
   226  
   227  // BasicAuth credentials
   228  type BasicAuth struct {
   229  	Username string
   230  	Password string
   231  	Context  config.Context
   232  }
   233  
   234  // GetOAuthToken from a Basic Auth flow
   235  func (b *BasicAuth) GetOAuthToken(ctx context.Context) (string, error) {
   236  	var apiClient = apihelper.New(b.Context)
   237  	var request = apiClient.URL(ctx, "/login")
   238  
   239  	request.Form("email", b.Username)
   240  	request.Form("password", b.Password)
   241  
   242  	if err := apihelper.Validate(request, request.Post()); err != nil {
   243  		return "", err
   244  	}
   245  
   246  	var data accessToken
   247  	var err = apihelper.DecodeJSON(request, &data)
   248  	return data.AccessToken, err
   249  }