Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New invitation endpoint #179

Merged
merged 5 commits into from
Nov 18, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,9 @@ func setEmailer(srv *Server, issuerName, fromAddress, emailerConfigFile string,
tMailer,
fromAddress,
srv.absURL(httpPathResetPassword),
srv.absURL(httpPathEmailVerify))
srv.absURL(httpPathEmailVerify),
srv.absURL(httpPathAcceptInvitation),
)

srv.UserEmailer = ue
return nil
Expand Down
4 changes: 2 additions & 2 deletions server/email_verification.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
// This handler is meant to be wrapped in clientTokenMiddleware, so a valid
// bearer token for the client is expected to be present.
// The user's JWT should be in the "token" parameter and the redirect URL should
// be in the "redirect_uri" param. Note that this re
// be in the "redirect_uri" param.
func handleVerifyEmailResendFunc(
issuerURL url.URL,
srvKeysFunc func() ([]key.PublicKey, error),
Expand Down Expand Up @@ -200,7 +200,7 @@ func handleEmailVerifyFunc(verifiedTpl *template.Template, issuer url.URL, keysF
if err != nil {
execTemplateWithStatus(w, verifiedTpl, emailVerifiedTemplateData{
Error: "There's been an error processing your request.",
Message: "Plesae try again later.",
Message: "Please try again later.",
}, http.StatusInternalServerError)
return
}
Expand Down
1 change: 1 addition & 0 deletions server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ var (
httpPathVerifyEmailResend = "/resend-verify-email"
httpPathSendResetPassword = "/send-reset-password"
httpPathResetPassword = "/reset-password"
httpPathAcceptInvitation = "/accept-invitation"
httpPathDebugVars = "/debug/vars"

cookieLastSeen = "LastSeen"
Expand Down
99 changes: 99 additions & 0 deletions server/invitation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package server

import (
"net/http"
"net/url"
"time"

"github.com/coreos/dex/pkg/log"
"github.com/coreos/dex/user"
"github.com/coreos/go-oidc/jose"
"github.com/coreos/go-oidc/key"
)

type invitationTemplateData struct {
Error, Message string
}

type InvitationHandler struct {
issuerURL url.URL
passwordResetURL url.URL
um *user.Manager
keysFunc func() ([]key.PublicKey, error)
signerFunc func() (jose.Signer, error)
redirectValidityWindow time.Duration
}

func (h *InvitationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
h.handleGET(w, r)
default:
writeAPIError(w, http.StatusMethodNotAllowed, newAPIError(errorInvalidRequest,
"method not allowed"))
}
}

func (h *InvitationHandler) handleGET(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
token := q.Get("token")

keys, err := h.keysFunc()
if err != nil {
log.Errorf("internal error getting public keys: %v", err)
writeAPIError(w, http.StatusInternalServerError, newAPIError(errorServerError,
"There's been an error processing your request."))
return
}

invite, err := user.ParseAndVerifyInvitationToken(token, h.issuerURL, keys)
if err != nil {
log.Debugf("invalid invitation token: %v (%v)", err, token)
writeAPIError(w, http.StatusBadRequest, newAPIError(errorInvalidRequest,
"Your invitation could not be verified"))
return
}

_, err = h.um.VerifyEmail(invite)
if err != nil && err != user.ErrorEmailAlreadyVerified {
// Allow AlreadyVerified folks to pass through- otherwise
// folks who encounter an error after passing this point will
// never be able to set their passwords.
log.Debugf("error attempting to verify email: %v", err)
switch err {
case user.ErrorEVEmailDoesntMatch:
writeAPIError(w, http.StatusBadRequest, newAPIError(errorInvalidRequest,
"Your email does not match the email address on file"))
return
default:
log.Errorf("internal error verifying email: %v", err)
writeAPIError(w, http.StatusInternalServerError, newAPIError(errorServerError,
"There's been an error processing your request."))
return
}
}

passwordReset := invite.PasswordReset(h.issuerURL, h.redirectValidityWindow)
signer, err := h.signerFunc()
if err != nil || signer == nil {
log.Errorf("error getting signer: %v (signer: %v)", err, signer)
writeAPIError(w, http.StatusInternalServerError, newAPIError(errorServerError,
"There's been an error processing your request."))
return
}

jwt, err := jose.NewSignedJWT(passwordReset.Claims, signer)
if err != nil {
log.Errorf("error constructing or signing PasswordReset from Invitation JWT: %v", err)
writeAPIError(w, http.StatusInternalServerError, newAPIError(errorServerError,
"There's been an error processing your request."))
return
}
passwordResetToken := jwt.Encode()

passwordResetURL := h.passwordResetURL
newQuery := passwordResetURL.Query()
newQuery.Set("token", passwordResetToken)
passwordResetURL.RawQuery = newQuery.Encode()
http.Redirect(w, r, passwordResetURL.String(), http.StatusSeeOther)
}
189 changes: 189 additions & 0 deletions server/invitation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package server

import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"

"github.com/jonboulle/clockwork"

"github.com/coreos/dex/user"
"github.com/coreos/go-oidc/jose"
"github.com/coreos/go-oidc/key"
)

var (
clock = clockwork.NewRealClock()
)

func TestInvitationHandler(t *testing.T) {
invUserID := "ID-1"
invVerifiedID := "ID-Verified"
invGoodSigner := key.NewPrivateKeySet([]*key.PrivateKey{testPrivKey},
time.Now().Add(time.Minute)).Active().Signer()

badKey, err := key.GeneratePrivateKey()
if err != nil {
panic(fmt.Sprintf("couldn't make new key: %q", err))
}

invBadSigner := key.NewPrivateKeySet([]*key.PrivateKey{badKey},
time.Now().Add(time.Minute)).Active().Signer()

makeInvitationToken := func(password, userID, clientID, email string, callback url.URL, expires time.Duration, signer jose.Signer) string {
iv := user.NewInvitation(
user.User{ID: userID, Email: email},
user.Password(password),
testIssuerURL,
clientID,
callback,
expires)

jwt, err := jose.NewSignedJWT(iv.Claims, signer)
if err != nil {
t.Fatalf("couldn't make token: %q", err)
}
token := jwt.Encode()
return token
}

tests := []struct {
userID string
query url.Values
signer jose.Signer
wantCode int
wantCallback url.URL
wantEmailVerified bool
}{
{ // Case 0 Happy Path
userID: invUserID,
query: url.Values{
"token": []string{makeInvitationToken("password", invUserID, testClientID, "[email protected]", testRedirectURL, time.Hour*1, invGoodSigner)},
},
signer: invGoodSigner,
wantCode: http.StatusSeeOther,
wantCallback: testRedirectURL,
wantEmailVerified: true,
},
{ // Case 1 user already verified
userID: invVerifiedID,
query: url.Values{
"token": []string{makeInvitationToken("password", invVerifiedID, testClientID, "[email protected]", testRedirectURL, time.Hour*1, invGoodSigner)},
},
signer: invGoodSigner,
wantCode: http.StatusSeeOther,
wantCallback: testRedirectURL,
wantEmailVerified: true,
},
{ // Case 2 bad email
userID: invUserID,
query: url.Values{
"token": []string{makeInvitationToken("password", invVerifiedID, testClientID, "[email protected]", testRedirectURL, time.Hour*1, invGoodSigner)},
},
signer: invGoodSigner,
wantCode: http.StatusBadRequest,
wantCallback: testRedirectURL,
wantEmailVerified: false,
},
{ // Case 3 bad signer
userID: invUserID,
query: url.Values{
"token": []string{makeInvitationToken("password", invUserID, testClientID, "[email protected]", testRedirectURL, time.Hour*1, invBadSigner)},
},
signer: invGoodSigner,
wantCode: http.StatusBadRequest,
wantCallback: testRedirectURL,
wantEmailVerified: false,
},
}

for i, tt := range tests {
f, err := makeTestFixtures()
if err != nil {
t.Fatalf("case %d: could not make test fixtures: %v", i, err)
}

keys, err := f.srv.KeyManager.PublicKeys()
if err != nil {
t.Fatalf("case %d: test fixture key infrastructure is broken: %v", i, err)
}

tZero := clock.Now()
handler := &InvitationHandler{
passwordResetURL: f.srv.absURL("RESETME"),
issuerURL: testIssuerURL,
um: f.srv.UserManager,
keysFunc: f.srv.KeyManager.PublicKeys,
signerFunc: func() (jose.Signer, error) { return tt.signer, nil },
redirectValidityWindow: 100 * time.Second,
}

w := httptest.NewRecorder()
u := testIssuerURL
u.RawQuery = tt.query.Encode()
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
t.Fatalf("case %d: impossible error: %v", i, err)
}

handler.ServeHTTP(w, req)

if tt.wantCode != w.Code {
t.Errorf("case %d: wantCode=%v, got=%v", i, tt.wantCode, w.Code)
continue
}

usr, err := f.srv.UserManager.Get(tt.userID)
if err != nil {
t.Fatalf("case %d: unexpected error: %v", i, err)
}

if usr.EmailVerified != tt.wantEmailVerified {
t.Errorf("case %d: wantEmailVerified=%v got=%v", i, tt.wantEmailVerified, usr.EmailVerified)
}

if w.Code == http.StatusSeeOther {
locString := w.HeaderMap.Get("Location")
loc, err := url.Parse(locString)
if err != nil {
t.Fatalf("case %d: redirect returned nonsense url: '%v', %v", i, locString, err)
}

pwrToken := loc.Query().Get("token")
pwrReset, err := user.ParseAndVerifyPasswordResetToken(pwrToken, testIssuerURL, keys)
if err != nil {
t.Errorf("case %d: password token is invalid: %v", i, err)
}

expTime := pwrReset.Claims["exp"].(float64)
if expTime > float64(tZero.Add(handler.redirectValidityWindow).Unix()) ||
expTime < float64(tZero.Unix()) {
t.Errorf("case %d: funny expiration time detected: %d", i, pwrReset.Claims["exp"])
}

if pwrReset.Claims["aud"] != testClientID {
t.Errorf("case %d: wanted \"aud\"=%v got=%v", i, testClientID, pwrReset.Claims["aud"])
}

if pwrReset.Claims["iss"] != testIssuerURL.String() {
t.Errorf("case %d: wanted \"iss\"=%v got=%v", i, testIssuerURL, pwrReset.Claims["iss"])
}

if pwrReset.UserID() != tt.userID {
t.Errorf("case %d: wanted UserID=%v got=%v", i, tt.userID, pwrReset.UserID())
}

if bytes.Compare(pwrReset.Password(), user.Password("password")) != 0 {
t.Errorf("case %d: wanted Password=%v got=%v", i, user.Password("password"), pwrReset.Password())
}

if *pwrReset.Callback() != testRedirectURL {
t.Errorf("case %d: wanted callback=%v got=%v", i, testRedirectURL, pwrReset.Callback())
}
}
}
}
3 changes: 1 addition & 2 deletions server/password.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,11 +250,10 @@ func (r *resetPasswordRequest) handlePOST() {
return
default:
r.data.Error = "Error Processing Request"
r.data.Message = "Plesae try again later."
r.data.Message = "Please try again later."
execTemplateWithStatus(r.w, r.h.tpl, r.data, http.StatusInternalServerError)
return
}

}
if cbURL == nil {
r.data.Success = true
Expand Down
2 changes: 1 addition & 1 deletion server/password_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ func TestSendResetPasswordEmailHandler(t *testing.T) {

func TestResetPasswordHandler(t *testing.T) {
makeToken := func(userID, password, clientID string, callback url.URL, expires time.Duration, signer jose.Signer) string {
pr := user.NewPasswordReset(user.User{ID: "ID-1"},
pr := user.NewPasswordReset("ID-1",
user.Password(password),
testIssuerURL,
clientID,
Expand Down
9 changes: 9 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,15 @@ func (s *Server) HTTPHandler() http.Handler {
keysFunc: s.KeyManager.PublicKeys,
})

mux.Handle(httpPathAcceptInvitation, &InvitationHandler{
passwordResetURL: s.absURL(httpPathResetPassword),
issuerURL: s.IssuerURL,
um: s.UserManager,
keysFunc: s.KeyManager.PublicKeys,
signerFunc: s.KeyManager.Signer,
redirectValidityWindow: s.SessionManager.ValidityWindow,
})

mux.HandleFunc(httpPathDebugVars, health.ExpvarHandler)

pcfg := s.ProviderConfig()
Expand Down
Loading