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 }