login.go 10.7 KB
Newer Older
ale's avatar
ale committed
1 2 3 4 5 6 7
package server

import (
	"bytes"
	"encoding/gob"
	"encoding/json"
	"errors"
ale's avatar
ale committed
8
	"fmt"
ale's avatar
ale committed
9 10 11
	"html/template"
	"log"
	"net/http"
12
	"net/url"
ale's avatar
ale committed
13 14 15 16 17 18
	"strings"
	"time"

	"github.com/gorilla/csrf"
	"github.com/gorilla/sessions"
	"github.com/tstranex/u2f"
19
	"go.opencensus.io/trace"
ale's avatar
ale committed
20 21 22

	"git.autistici.org/id/auth"
	authclient "git.autistici.org/id/auth/client"
ale's avatar
ale committed
23
	"git.autistici.org/id/go-sso/httputil"
ale's avatar
ale committed
24 25 26 27 28 29
	"git.autistici.org/id/go-sso/server/device"
)

const loginSessionKey = "_login"

type loginSession struct {
ale's avatar
ale committed
30
	*httputil.ExpiringSession
ale's avatar
ale committed
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52

	State loginState

	// Post-login redirection URL.
	Redir string

	// Cached from the first form.
	Username string
	Password string

	// Cached from the first auth.Response.
	U2FSignRequest *u2f.WebSignRequest

	// Never actually serialized, just passed on
	// loginStateSuccess.
	UserInfo *auth.UserInfo
}

var defaultLoginSessionLifetime = 300 * time.Second

func newLoginSession() *loginSession {
	return &loginSession{
ale's avatar
ale committed
53
		ExpiringSession: httputil.NewExpiringSession(defaultLoginSessionLifetime),
ale's avatar
ale committed
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
		State:           loginStatePassword,
	}
}

type loginState int

const (
	loginStateNone = iota
	loginStatePassword
	loginStateOTP
	loginStateU2F
	loginStateSuccess
)

func init() {
	gob.Register(&loginSession{})
}

72
type loginCallbackFunc func(http.ResponseWriter, *http.Request, string, string, *auth.UserInfo) error
ale's avatar
ale committed
73 74

type loginHandler struct {
ale's avatar
ale committed
75 76 77 78 79 80 81 82 83
	authClient         authclient.Client
	authService        string
	u2fAppID           string
	urlPrefix          string
	devMgr             *device.Manager
	loginCallback      loginCallbackFunc
	loginSessionStore  sessions.Store
	tpl                *template.Template
	accountRecoveryURL string
ale's avatar
ale committed
84 85 86 87
}

// NewLoginHandler will wrap an http.Handler with the login workflow,
// invoking it only on successful login.
ale's avatar
ale committed
88
func newLoginHandler(okHandler loginCallbackFunc, devMgr *device.Manager, authClient authclient.Client, authService, u2fAppID, urlPrefix, accountRecoveryURL string, tpl *template.Template, keyPairs ...[]byte) *loginHandler {
ale's avatar
ale committed
89 90 91 92 93 94 95
	store := sessions.NewCookieStore(keyPairs...)
	store.Options = &sessions.Options{
		HttpOnly: true,
		Secure:   true,
		MaxAge:   0,
	}
	return &loginHandler{
ale's avatar
ale committed
96 97 98 99 100 101 102 103 104
		authClient:         authClient,
		authService:        authService,
		u2fAppID:           u2fAppID,
		urlPrefix:          strings.TrimRight(urlPrefix, "/"),
		devMgr:             devMgr,
		loginCallback:      okHandler,
		loginSessionStore:  store,
		accountRecoveryURL: accountRecoveryURL,
		tpl:                parseEmbeddedTemplates(),
ale's avatar
ale committed
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
	}
}

// The login session controls the flow of the client - it's just a way
// to ensure that every step is authorized as part of the login
// sequence.
func (l *loginHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	// Either fetch the current session or create a new blank one.
	httpSession, err := l.loginSessionStore.Get(req, loginSessionKey)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	session, ok := httpSession.Values["ls"].(*loginSession)
	if !ok || session == nil || !session.Valid() {
		session = newLoginSession()
		httpSession.Values["ls"] = session
	}

	// Dispatch the current state to its handler. Handlers will
	// handle the current request and either 1) validate the
	// request successfully and move to the next state, or 2)
	// return a response to the user. Handlers fall through to the
	// next state on success.
	for {
		newState, body, err := l.dispatch(w, req, session)

		// Uncaught errors result in 500s.
		if err != nil {
			log.Printf("login error: %v", err)
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		switch newState {
		case loginStateSuccess:
141 142 143
			// Successful login. Delete the login session and invoke
			// the login callback, before redirecting to the
			// original URL.
ale's avatar
ale committed
144
			httpSession.Options.MaxAge = -1
145 146 147 148 149
			if err := httpSession.Save(req, w); err != nil {
				log.Printf("login error saving session: %v", err)
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
150
			if err := l.loginCallback(w, req, session.Username, session.Password, session.UserInfo); err != nil {
ale's avatar
ale committed
151 152 153 154 155 156 157 158 159 160 161 162 163
				log.Printf("login callback error: %v: user=%s", err, session.Username)
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			http.Redirect(w, req, session.Redir, http.StatusFound)
			return

		case loginStateNone:
			if err := httpSession.Save(req, w); err != nil {
				log.Printf("error saving login session: %v", err)
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
164
			w.Write(body) // nolint
ale's avatar
ale committed
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
			return

		default:
			// Fall through to the next handler.
			session.State = newState
		}
	}
}

func (l *loginHandler) dispatch(w http.ResponseWriter, req *http.Request, session *loginSession) (loginState, []byte, error) {
	switch session.State {
	case loginStatePassword:
		return l.handlePassword(w, req, session)
	case loginStateOTP:
		return l.handleOTP(w, req, session)
	case loginStateU2F:
		return l.handleU2F(w, req, session)
	}
	return loginStateNone, nil, errors.New("unreachable")
}

// Handle password-based login.
func (l *loginHandler) handlePassword(w http.ResponseWriter, req *http.Request, session *loginSession) (loginState, []byte, error) {
	username := req.FormValue("username")
	password := req.FormValue("password")

	if req.Method == "GET" && session.Redir == "" {
		session.Redir = req.FormValue("r")
		// Enforce relative redirect URL (no host specified).
		if session.Redir == "" || !strings.HasPrefix(session.Redir, "/") {
			return loginStateNone, nil, errors.New("bad request")
		}
	}

	// If the request is a POST, attempt login with username/password.
	env := map[string]interface{}{
		"Error":    false,
		"Username": username,
	}
	if req.Method == "POST" && username != "" && password != "" {
		resp, err := l.makeAuthRequest(w, req, username, password, "", nil)
		if err != nil {
			return loginStateNone, nil, err
		}
209 210
		// Save username / password for later in case of
		// successful (or partially succesful) result.
ale's avatar
ale committed
211 212 213
		switch resp.Status {
		case auth.StatusOK:
			session.Username = username
214
			session.Password = password
ale's avatar
ale committed
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
			session.UserInfo = resp.UserInfo
			return loginStateSuccess, nil, nil
		case auth.StatusInsufficientCredentials:
			session.Username = username
			session.Password = password
			// If there is a U2F challenge in the auth
			// response, store it so we can render it
			// later.
			var nextState loginState = loginStateOTP
			if resp.TFAMethod == auth.TFAMethodU2F {
				nextState = loginStateU2F
				session.U2FSignRequest = resp.U2FSignRequest
			}
			return nextState, nil, nil
		}
		env["Error"] = true
	}

	return l.executeTemplateToBuffer(req, "login_password.html", env)
}

// Handle login with password and TOTP.
func (l *loginHandler) handleOTP(w http.ResponseWriter, req *http.Request, session *loginSession) (loginState, []byte, error) {
	otp := req.FormValue("otp")

	env := map[string]interface{}{"Error": false}
	if req.Method == "POST" && otp != "" {
		resp, err := l.makeAuthRequest(w, req, session.Username, session.Password, otp, nil)
		if err != nil {
			return loginStateNone, nil, err
		}
		if resp.Status == auth.StatusOK {
			session.UserInfo = resp.UserInfo
			return loginStateSuccess, nil, nil
		}
		env["Error"] = true
	}

	return l.executeTemplateToBuffer(req, "login_otp.html", env)
}

// Handle login with password and hardware token.
func (l *loginHandler) handleU2F(w http.ResponseWriter, req *http.Request, session *loginSession) (loginState, []byte, error) {
	u2fresponse := req.FormValue("u2f_response")

	env := map[string]interface{}{
		"U2FSignRequest": session.U2FSignRequest,
		"Error":          false,
	}
	if req.Method == "POST" && u2fresponse != "" {
		var usr u2f.SignResponse
		if err := json.Unmarshal([]byte(u2fresponse), &usr); err != nil {
			return loginStateNone, nil, err
		}

		resp, err := l.makeAuthRequest(w, req, session.Username, session.Password, "", &usr)
		if err != nil {
			return loginStateNone, nil, err
		}
		if resp.Status == auth.StatusOK {
			session.UserInfo = resp.UserInfo
			return loginStateSuccess, nil, nil
		}
		env["Error"] = true
	}

	return l.executeTemplateToBuffer(req, "login_u2f.html", env)
}

// Make the auth request to the authentication server.
func (l *loginHandler) makeAuthRequest(w http.ResponseWriter, req *http.Request, username, password, otp string, u2fResponse *u2f.SignResponse) (*auth.Response, error) {
ale's avatar
ale committed
286 287 288 289
	appID := l.u2fAppID
	if appID == "" {
		appID = u2fAppIDFromRequest(req)
	}
ale's avatar
ale committed
290 291 292 293 294 295
	ar := auth.Request{
		Service:     l.authService,
		Username:    username,
		Password:    []byte(password),
		OTP:         otp,
		DeviceInfo:  l.devMgr.GetDeviceInfoFromRequest(w, req),
ale's avatar
ale committed
296
		U2FResponse: u2fResponse,
ale's avatar
ale committed
297
		U2FAppID:    appID,
ale's avatar
ale committed
298
	}
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323

	// Trace the authentication request.
	ctx, span := trace.StartSpan(req.Context(), "auth",
		trace.WithSpanKind(trace.SpanKindClient))
	span.AddAttributes(
		trace.StringAttribute("auth.user", username),
		trace.StringAttribute("auth.service", l.authService),
		trace.BoolAttribute("auth.with_password", len(password) > 0),
		trace.BoolAttribute("auth.with_otp", otp != ""),
		trace.BoolAttribute("auth.with_u2f", u2fResponse != nil),
	)
	defer span.End()

	resp, err := l.authClient.Authenticate(ctx, &ar)

	// Record the authentication response status in the trace.
	if err != nil {
		span.SetStatus(trace.Status{Code: trace.StatusCodeUnknown, Message: err.Error()})
	} else if resp.Status == auth.StatusOK {
		span.SetStatus(trace.Status{Code: trace.StatusCodeOK, Message: "OK"})
	} else {
		span.SetStatus(trace.Status{Code: trace.StatusCodePermissionDenied, Message: resp.Status.String()})
	}

	return resp, err
ale's avatar
ale committed
324 325
}

326 327 328 329 330 331 332 333
// Return a (relative) URL that will redirect the user to the login
// page and set the continue token to the original requested URL.
func (l *loginHandler) makeLoginURL(req *http.Request) string {
	v := make(url.Values)
	v.Set("r", req.URL.Path+"?"+req.URL.RawQuery)
	return fmt.Sprintf("%s/login?%s", l.urlPrefix, v.Encode())
}

ale's avatar
ale committed
334 335 336 337
// Renders a template to a buffer, with a return value that makes it
// convenient to use in login handlers.
func (l *loginHandler) executeTemplateToBuffer(req *http.Request, templateName string, data map[string]interface{}) (loginState, []byte, error) {
	data["CSRFField"] = csrf.TemplateField(req)
338
	data["URLPrefix"] = l.urlPrefix
ale's avatar
ale committed
339
	data["AccountRecoveryURL"] = l.accountRecoveryURL
ale's avatar
ale committed
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
	var buf bytes.Buffer
	if err := l.tpl.ExecuteTemplate(&buf, templateName, data); err != nil {
		return loginStateNone, nil, err
	}
	return loginStateNone, buf.Bytes(), nil
}

// Template helper function that encodes its input as JSON.
func toJSON(obj interface{}) string {
	data, err := json.Marshal(obj)
	if err != nil {
		return ""
	}
	return string(data)
}
ale's avatar
ale committed
355 356 357 358 359

// Guess the correct U2F AppID from the HTTP request.
func u2fAppIDFromRequest(r *http.Request) string {
	return fmt.Sprintf("https://%s", r.Host)
}