forked from mystiq/dex
Move manager to it's own package so it can import db. Move all references to the in memory session repos to use sqlite3.
375 lines
9.2 KiB
Go
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
|
|
}
|