dex/server/register.go
Eric Chiang 7bac93aa20 *: remove in memory session repos
Move manager to it's own package so it can import db. Move all
references to the in memory session repos to use sqlite3.
2016-02-12 13:19:05 -08:00

375 lines
9.2 KiB
Go

package server
import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/coreos/dex/connector"
"github.com/coreos/dex/pkg/log"
"github.com/coreos/dex/session"
sessionmanager "github.com/coreos/dex/session/manager"
"github.com/coreos/dex/user"
usermanager "github.com/coreos/dex/user/manager"
"github.com/coreos/go-oidc/oidc"
)
type formError struct {
Field string
Error string
}
type remoteExistsData struct {
Login string
Register string
}
type registerTemplateData struct {
Error bool
FormErrors []formError
Message string
Email string
Code string
Password string
Local bool
RemoteExists *remoteExistsData
}
var (
errToFormErrorMap = map[error]formError{
user.ErrorInvalidEmail: formError{
Field: "email",
Error: "Please enter a valid email",
},
user.ErrorInvalidPassword: formError{
Field: "password",
Error: "Please enter a valid password",
},
user.ErrorDuplicateEmail: formError{
Field: "email",
Error: "That email is already in use; please choose another.",
},
}
)
func handleRegisterFunc(s *Server, tpl Template) http.HandlerFunc {
errPage := func(w http.ResponseWriter, msg string, code string, status int) {
data := registerTemplateData{
Error: true,
Message: msg,
Code: code,
}
execTemplateWithStatus(w, tpl, data, status)
}
internalError := func(w http.ResponseWriter, err error) {
log.Errorf("Internal Error during registration: %v", err)
errPage(w, "There was a problem processing your request.", "", http.StatusInternalServerError)
}
idx := makeConnectorMap(s.Connectors)
return func(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
internalError(w, err)
return
}
// verify the user has a valid code.
key := r.Form.Get("code")
sessionID, err := s.SessionManager.ExchangeKey(key)
if err != nil {
errPage(w, "Please authenticate before registering.", "", http.StatusUnauthorized)
return
}
// create a new code for them to use next time they hit the server.
code, err := s.SessionManager.NewSessionKey(sessionID)
if err != nil {
internalError(w, err)
return
}
ses, err := s.SessionManager.Get(sessionID)
if err != nil || ses == nil {
return
}
var exists bool
exists, err = remoteIdentityExists(s.UserRepo, ses.ConnectorID, ses.Identity.ID)
if err != nil {
internalError(w, err)
return
}
if exists {
// we have to create a new session to be able to run the server.Login function
newSessionKey, err := s.NewSession(ses.ConnectorID, ses.ClientID,
ses.ClientState, ses.RedirectURL, ses.Nonce, false, ses.Scope)
if err != nil {
internalError(w, err)
return
}
// make sure to clean up the old session
if err = s.KillSession(code); err != nil {
internalError(w, err)
}
// finally, we can create a valid redirect URL for them.
redirURL, err := s.Login(ses.Identity, newSessionKey)
if err != nil {
internalError(w, err)
return
}
registerURL := newLoginURLFromSession(
s.IssuerURL, ses, true, []string{}, "")
execTemplate(w, tpl, registerTemplateData{
RemoteExists: &remoteExistsData{
Login: redirURL,
Register: registerURL.String(),
},
})
return
}
// determine whether or not this is a local or remote ID that is going
// to be registered.
idpc, ok := idx[ses.ConnectorID]
if !ok {
internalError(w, fmt.Errorf("no such IDPC: %v", ses.ConnectorID))
return
}
_, local := idpc.(*connector.LocalConnector)
// Does the email comes from a trusted provider?
trustedEmail := ses.Identity.Email != "" && idpc.TrustedEmailProvider()
validate := r.Form.Get("validate") == "1"
formErrors := []formError{}
email := strings.TrimSpace(r.Form.Get("email"))
// only auto-populate the first time the page is GETted, not on
// subsequent POSTs
if email == "" && r.Method == "GET" {
email = ses.Identity.Email
}
password := r.Form.Get("password")
if validate {
if email == "" || !user.ValidEmail(email) {
formErrors = append(formErrors, formError{"email", "Please supply a valid email"})
}
if local && password == "" {
formErrors = append(formErrors, formError{"password", "Please supply a valid password"})
}
}
data := registerTemplateData{
Code: code,
Email: email,
Password: password,
Local: local,
}
// If there are form errors or this is the initial request
// (i.e. validate==false), and we are not going to auto-submit a
// trusted email, then show the form.
if (len(formErrors) > 0 || !validate) && !trustedEmail {
data.FormErrors = formErrors
if !validate {
execTemplate(w, tpl, data)
} else {
execTemplateWithStatus(w, tpl, data, http.StatusBadRequest)
}
return
}
var userID string
if local {
userID, err = registerFromLocalConnector(
s.UserManager,
s.SessionManager,
ses,
email, password)
} else {
if trustedEmail {
// in the case of a trusted email provider, make sure we are
// getting the email address from the session, not from the
// query string, to prevent forgeries.
email = ses.Identity.Email
}
userID, err = registerFromRemoteConnector(
s.UserManager,
ses,
email,
trustedEmail)
}
if err == user.ErrorDuplicateEmail {
// In this case, the user probably just forgot that they registered.
connID, err := getConnectorForUserByEmail(s.UserRepo, email)
if err != nil {
internalError(w, err)
}
loginURL := newLoginURLFromSession(
s.IssuerURL, ses, false, []string{connID}, "login-maybe")
if err = s.KillSession(code); err != nil {
log.Errorf("Error killing session: %v", err)
}
http.Redirect(w, r, loginURL.String(), http.StatusSeeOther)
return
}
if err != nil {
formErrors := errToFormErrors(err)
if len(formErrors) > 0 {
data.FormErrors = formErrors
execTemplate(w, tpl, data)
return
}
if err == user.ErrorDuplicateRemoteIdentity {
errPage(w, "You already registered an account with this identity", "", http.StatusConflict)
return
}
internalError(w, err)
return
}
ses, err = s.SessionManager.AttachUser(sessionID, userID)
if err != nil {
internalError(w, err)
return
}
usr, err := s.UserRepo.Get(nil, userID)
if err != nil {
internalError(w, err)
return
}
if !trustedEmail {
_, err = s.UserEmailer.SendEmailVerification(usr.ID, ses.ClientID, ses.RedirectURL)
if err != nil {
log.Errorf("Error sending email verification: %v", err)
}
}
w.Header().Set("Location", makeClientRedirectURL(
ses.RedirectURL, code, ses.ClientState).String())
w.WriteHeader(http.StatusSeeOther)
return
}
}
func makeClientRedirectURL(baseRedirURL url.URL, code, clientState string) *url.URL {
ru := baseRedirURL
q := ru.Query()
q.Set("code", code)
q.Set("state", clientState)
ru.RawQuery = q.Encode()
return &ru
}
func registerFromLocalConnector(userManager *usermanager.UserManager, sessionManager *sessionmanager.SessionManager, ses *session.Session, email, password string) (string, error) {
userID, err := userManager.RegisterWithPassword(email, password, ses.ConnectorID)
if err != nil {
return "", err
}
ses, err = sessionManager.AttachRemoteIdentity(ses.ID, oidc.Identity{
ID: userID,
})
if err != nil {
return "", err
}
return userID, nil
}
func registerFromRemoteConnector(userManager *usermanager.UserManager, ses *session.Session, email string, emailVerified bool) (string, error) {
if ses.Identity.ID == "" {
return "", errors.New("No Identity found in session.")
}
rid := user.RemoteIdentity{
ConnectorID: ses.ConnectorID,
ID: ses.Identity.ID,
}
userID, err := userManager.RegisterWithRemoteIdentity(email, emailVerified, rid)
if err != nil {
return "", err
}
return userID, nil
}
func errToFormErrors(err error) []formError {
fes := []formError{}
fe, ok := errToFormErrorMap[err]
if ok {
fes = append(fes, fe)
}
return fes
}
func getConnectorForUserByEmail(ur user.UserRepo, email string) (string, error) {
usr, err := ur.GetByEmail(nil, email)
if err != nil {
return "", err
}
rids, err := ur.GetRemoteIdentities(nil, usr.ID)
if err != nil {
return "", err
}
if len(rids) == 0 {
return "", fmt.Errorf("No remote Identities for user %v", usr.ID)
}
return rids[0].ConnectorID, nil
}
func newLoginURLFromSession(issuer url.URL, ses *session.Session, register bool, connectorFilter []string, msgCode string) *url.URL {
loginURL := issuer
v := loginURL.Query()
loginURL.Path = httpPathAuth
v.Set("redirect_uri", ses.RedirectURL.String())
v.Set("state", ses.ClientState)
v.Set("client_id", ses.ClientID)
if register {
v.Set("register", "1")
}
if len(connectorFilter) > 0 {
v.Set("show_connectors", strings.Join(connectorFilter, ","))
}
if msgCode != "" {
v.Set("msg_code", msgCode)
}
if len(ses.Scope) > 0 {
v.Set("scope", strings.Join(ses.Scope, " "))
}
loginURL.RawQuery = v.Encode()
return &loginURL
}
func remoteIdentityExists(ur user.UserRepo, connectorID, id string) (bool, error) {
_, err := ur.GetByRemoteIdentity(nil, user.RemoteIdentity{
ConnectorID: connectorID,
ID: id,
})
if err == user.ErrorNotFound {
return false, nil
}
if err == nil {
return true, nil
}
return false, err
}