Commit 70eb87d8 authored by Joe Bowers's avatar Joe Bowers

Merge pull request #158 from joeatwork/share-token-code

server: Share token code
parents fd814dd6 b1e43698
...@@ -20,7 +20,9 @@ fi ...@@ -20,7 +20,9 @@ fi
rm -rf $GOPATH/src/github.com/coreos/dex rm -rf $GOPATH/src/github.com/coreos/dex
mkdir -p $GOPATH/src/github.com/coreos/ mkdir -p $GOPATH/src/github.com/coreos/
ln -s ${PWD} $GOPATH/src/github.com/coreos/dex
# Only attempt to link dex into godeps if it isn't already there
[ -d $GOPATH/src/github.com/coreos/dex ] || ln -s ${PWD} $GOPATH/src/github.com/coreos/dex
LD_FLAGS="-X main.version=$(git rev-parse HEAD)" LD_FLAGS="-X main.version=$(git rev-parse HEAD)"
go build -o bin/dex-worker -ldflags="$LD_FLAGS" github.com/coreos/dex/cmd/dex-worker go build -o bin/dex-worker -ldflags="$LD_FLAGS" github.com/coreos/dex/cmd/dex-worker
......
...@@ -66,31 +66,49 @@ func (h *SendResetPasswordEmailHandler) handleGET(w http.ResponseWriter, r *http ...@@ -66,31 +66,49 @@ func (h *SendResetPasswordEmailHandler) handleGET(w http.ResponseWriter, r *http
log.Errorf("could not exchange sessionKey: %v", err) log.Errorf("could not exchange sessionKey: %v", err)
} }
data := sendResetPasswordEmailData{} data := sendResetPasswordEmailData{}
h.fillData(r, &data) if err := h.fillData(r, &data); err != nil {
writeAPIError(w, http.StatusBadRequest, err)
}
if data.ClientID == "" {
writeAPIError(w, http.StatusBadRequest, newAPIError(errorInvalidRequest,
"missing required parameters"))
return
}
execTemplate(w, h.tpl, data) execTemplate(w, h.tpl, data)
} }
func (h *SendResetPasswordEmailHandler) fillData(r *http.Request, data *sendResetPasswordEmailData) { func (h *SendResetPasswordEmailHandler) fillData(r *http.Request, data *sendResetPasswordEmailData) *apiError {
data.Email = r.FormValue("email") data.Email = r.FormValue("email")
clientID := r.FormValue("client_id") data.ClientID = r.FormValue("client_id")
redirectURL := r.FormValue("redirect_uri") redirectURL := r.FormValue("redirect_uri")
if redirectURL != "" && clientID != "" { if redirectURL != "" && data.ClientID != "" {
if parsed, ok := h.validateRedirectURL(clientID, redirectURL); ok { if parsed, ok := h.validateRedirectURL(data.ClientID, redirectURL); ok {
data.ClientID = clientID
data.RedirectURL = redirectURL data.RedirectURL = redirectURL
data.RedirectURLParsed = parsed data.RedirectURLParsed = parsed
} else {
return newAPIError(errorInvalidRequest, "invalid redirect url")
} }
} }
return nil
} }
func (h *SendResetPasswordEmailHandler) handlePOST(w http.ResponseWriter, r *http.Request) { func (h *SendResetPasswordEmailHandler) handlePOST(w http.ResponseWriter, r *http.Request) {
data := sendResetPasswordEmailData{} data := sendResetPasswordEmailData{}
h.fillData(r, &data) if err := h.fillData(r, &data); err != nil {
writeAPIError(w, http.StatusBadRequest, err)
}
if data.ClientID == "" {
writeAPIError(w, http.StatusBadRequest, newAPIError(errorInvalidRequest, "client id missing"))
return
}
if !user.ValidEmail(data.Email) { if !user.ValidEmail(data.Email) {
h.errPage(w, "Please supply a valid email addresss.", http.StatusBadRequest, &data) h.errPage(w, "Please supply a valid email address.", http.StatusBadRequest, &data)
return return
} }
......
...@@ -48,7 +48,7 @@ func TestSendResetPasswordEmailHandler(t *testing.T) { ...@@ -48,7 +48,7 @@ func TestSendResetPasswordEmailHandler(t *testing.T) {
wantPRPassword string wantPRPassword string
}{ }{
// First we'll test all the requests for happy path #1: // First we'll test all the requests for happy path #1:
{ { // Case 0
// STEP 1.1 - User clicks on link from local-login page and has a // STEP 1.1 - User clicks on link from local-login page and has a
// session_key, which will prompt a redirect to page which has // session_key, which will prompt a redirect to page which has
...@@ -69,7 +69,7 @@ func TestSendResetPasswordEmailHandler(t *testing.T) { ...@@ -69,7 +69,7 @@ func TestSendResetPasswordEmailHandler(t *testing.T) {
}.Encode(), }.Encode(),
}, },
}, },
{ { // Case 1
// STEP 1.2 - This is the request that happens as a result of the // STEP 1.2 - This is the request that happens as a result of the
// redirect. The client_id and redirect_uri should be in the form on // redirect. The client_id and redirect_uri should be in the form on
...@@ -87,7 +87,7 @@ func TestSendResetPasswordEmailHandler(t *testing.T) { ...@@ -87,7 +87,7 @@ func TestSendResetPasswordEmailHandler(t *testing.T) {
"email": str(""), "email": str(""),
}, },
}, },
{ { // Case 2
// STEP 1.3 - User enters a valid email, gets success page. The // STEP 1.3 - User enters a valid email, gets success page. The
// values from the GET redirect are resent in the form POST along // values from the GET redirect are resent in the form POST along
// with the email. // with the email.
...@@ -109,25 +109,28 @@ func TestSendResetPasswordEmailHandler(t *testing.T) { ...@@ -109,25 +109,28 @@ func TestSendResetPasswordEmailHandler(t *testing.T) {
wantPRPassword: "password", wantPRPassword: "password",
}, },
// Happy Path #2 - same as above but without session_key // Happy Path #2 - no email or redirect
{ { // Case 3
// STEP 2.1 - user somehow ends up on reset page without a session_key // STEP 2.1 - user somehow ends up on reset page with nothing but a client id
query: url.Values{}, query: url.Values{
"client_id": str(testClientID),
},
method: "GET", method: "GET",
wantCode: http.StatusOK, wantCode: http.StatusOK,
wantFormValues: &url.Values{ wantFormValues: &url.Values{
"client_id": str(""), "client_id": str(testClientID),
"redirect_uri": str(""), "redirect_uri": str(""),
"email": str(""), "email": str(""),
}, },
}, },
{ { // Case 4
// STEP 2.3 - There is no STEP 2 because we don't have the redirect. // STEP 2.3 - There is no STEP 2 because we don't have the redirect.
query: url.Values{ query: url.Values{
"email": str("Email-1@example.com"), "email": str("Email-1@example.com"),
"client_id": str(testClientID),
}, },
method: "POST", method: "POST",
...@@ -142,7 +145,7 @@ func TestSendResetPasswordEmailHandler(t *testing.T) { ...@@ -142,7 +145,7 @@ func TestSendResetPasswordEmailHandler(t *testing.T) {
}, },
// Some error conditions: // Some error conditions:
{ { // Case 5
// STEP 1.3.1 - User enters an invalid email, gets form again. // STEP 1.3.1 - User enters an invalid email, gets form again.
query: url.Values{ query: url.Values{
"client_id": str(testClientID), "client_id": str(testClientID),
...@@ -158,7 +161,7 @@ func TestSendResetPasswordEmailHandler(t *testing.T) { ...@@ -158,7 +161,7 @@ func TestSendResetPasswordEmailHandler(t *testing.T) {
"email": str(""), "email": str(""),
}, },
}, },
{ { // Case 6
// STEP 1.3.2 - User enters a valid email but for a user not in the // STEP 1.3.2 - User enters a valid email but for a user not in the
// system. They still get the success page, but no email is sent. // system. They still get the success page, but no email is sent.
query: url.Values{ query: url.Values{
...@@ -170,55 +173,32 @@ func TestSendResetPasswordEmailHandler(t *testing.T) { ...@@ -170,55 +173,32 @@ func TestSendResetPasswordEmailHandler(t *testing.T) {
wantCode: http.StatusOK, wantCode: http.StatusOK,
}, },
{ { // Case 7
// STEP 1.3.3 - User enters a valid email but for a user not in the
// system. They still get the success page, but no email is sent.
query: url.Values{
"client_id": str(testClientID),
"redirect_uri": str(testRedirectURL.String()),
"email": str("NOSUCHUSER@example.com"),
},
method: "POST",
wantCode: http.StatusOK,
}, {
// STEP 1.1.1 - User clicks on link from local-login page and has a // STEP 1.1.1 - User clicks on link from local-login page and has a
// session_key, but it is not-recognized. There is no redirect, the // session_key, but it is not-recognized.
// user goes right to the form which has no client_id or
// redirect_uri
query: url.Values{ query: url.Values{
"session_key": str("code-UNKNOWN"), "session_key": str("code-UNKNOWN"),
}, },
method: "GET", method: "GET",
wantCode: http.StatusOK, wantCode: http.StatusBadRequest,
wantFormValues: &url.Values{
"client_id": str(""),
"redirect_uri": str(""),
"email": str(""),
}, },
}, { { // Case 8
// STEP 1.2.1 - Someone trying to replace a valid redirect_url with // STEP 1.2.1 - Someone trying to replace a valid redirect_url with
// an invalid one; in this case we just give them the form but // an invalid one.
// ignore client_id and redirect_uri.
query: url.Values{ query: url.Values{
"client_id": str(testClientID), "client_id": str(testClientID),
"redirect_uri": str("http://evilhackers.example.com"), "redirect_uri": str("http://evilhackers.example.com"),
}, },
method: "GET", method: "GET",
wantCode: http.StatusOK, wantCode: http.StatusBadRequest,
wantFormValues: &url.Values{
"client_id": str(""),
"redirect_uri": str(""),
"email": str(""),
}, },
}, { { // Case 9
// STEP 1.3.4 - User enters a valid email for a user in the system, // STEP 1.3.4 - User enters a valid email for a user in the system,
// but with an invalid redirect_uri. They still get an email, but // but with an invalid redirect_uri.
// with no redirect url.
query: url.Values{ query: url.Values{
"client_id": str(testClientID), "client_id": str(testClientID),
"redirect_uri": str("http://evilhackers.example.com"), "redirect_uri": str("http://evilhackers.example.com"),
...@@ -226,15 +206,43 @@ func TestSendResetPasswordEmailHandler(t *testing.T) { ...@@ -226,15 +206,43 @@ func TestSendResetPasswordEmailHandler(t *testing.T) {
}, },
method: "POST", method: "POST",
wantCode: http.StatusOK, wantCode: http.StatusBadRequest,
wantEmailer: &testEmailer{
to: str("Email-1@example.com"),
from: "noreply@example.com",
subject: "Reset your password.",
}, },
wantPRPassword: "password", { // Case 10
wantPRUserID: "ID-1",
wantPRRedirect: nil, // User hits the page with a valid email but no client id
query: url.Values{
"email": str("Email-1@example.com"),
},
method: "GET",
wantCode: http.StatusBadRequest,
},
{ // Case 10
// Don't send an email without a client id
query: url.Values{
"email": str("Email-1@example.com"),
},
method: "POST",
wantCode: http.StatusBadRequest,
},
{ // Case 11
// Empty requests lack a client id
query: url.Values{},
method: "GET",
wantCode: http.StatusBadRequest,
},
{ // Case 12
// Empty requests lack a client id
query: url.Values{},
method: "POST",
wantCode: http.StatusBadRequest,
}, },
} }
...@@ -348,23 +356,19 @@ func TestSendResetPasswordEmailHandler(t *testing.T) { ...@@ -348,23 +356,19 @@ func TestSendResetPasswordEmailHandler(t *testing.T) {
} }
func TestResetPasswordHandler(t *testing.T) { func TestResetPasswordHandler(t *testing.T) {
makeToken := func(userID, password string, callback url.URL, expires time.Duration, signer jose.Signer) string { makeToken := func(userID, password, clientID string, callback url.URL, expires time.Duration, signer jose.Signer) string {
var clientID string
if callback.String() == "" {
clientID = ""
} else {
clientID = testClientID
}
pr := user.NewPasswordReset(user.User{ID: "ID-1"}, pr := user.NewPasswordReset(user.User{ID: "ID-1"},
user.Password(password), user.Password(password),
testIssuerURL, testIssuerURL,
clientID, clientID,
callback, callback,
expires) expires)
token, err := pr.Token(signer)
jwt, err := jose.NewSignedJWT(pr.Claims, signer)
if err != nil { if err != nil {
t.Fatalf("couldn't make token: %q", err) t.Fatalf("couldn't make token: %q", err)
} }
token := jwt.Encode()
return token return token
} }
goodSigner := key.NewPrivateKeySet([]*key.PrivateKey{testPrivKey}, goodSigner := key.NewPrivateKeySet([]*key.PrivateKey{testPrivKey},
...@@ -398,24 +402,24 @@ func TestResetPasswordHandler(t *testing.T) { ...@@ -398,24 +402,24 @@ func TestResetPasswordHandler(t *testing.T) {
wantPassword string wantPassword string
}{ }{
// Scenario 1: Happy Path // Scenario 1: Happy Path
{ { // Case 0
// Step 1.1 - User clicks link in email, has valid token. // Step 1.1 - User clicks link in email, has valid token.
query: url.Values{ query: url.Values{
"token": str(makeToken("ID-1", "password", testRedirectURL, time.Hour*1, goodSigner)), "token": str(makeToken("ID-1", "password", testClientID, testRedirectURL, time.Hour*1, goodSigner)),
}, },
method: "GET", method: "GET",
wantCode: http.StatusOK, wantCode: http.StatusOK,
wantFormValues: &url.Values{ wantFormValues: &url.Values{
"password": str(""), "password": str(""),
"token": str(makeToken("ID-1", "password", testRedirectURL, time.Hour*1, goodSigner)), "token": str(makeToken("ID-1", "password", testClientID, testRedirectURL, time.Hour*1, goodSigner)),
}, },
wantPassword: "password", wantPassword: "password",
}, },
{ { // Case 1
// Step 1.2 - User enters in new valid password, password is changed, user is redirected. // Step 1.2 - User enters in new valid password, password is changed, user is redirected.
query: url.Values{ query: url.Values{
"token": str(makeToken("ID-1", "password", testRedirectURL, time.Hour*1, goodSigner)), "token": str(makeToken("ID-1", "password", testClientID, testRedirectURL, time.Hour*1, goodSigner)),
"password": str("new_password"), "password": str("new_password"),
}, },
method: "POST", method: "POST",
...@@ -424,26 +428,25 @@ func TestResetPasswordHandler(t *testing.T) { ...@@ -424,26 +428,25 @@ func TestResetPasswordHandler(t *testing.T) {
wantFormValues: &url.Values{}, wantFormValues: &url.Values{},
wantPassword: "NEW_PASSWORD", wantPassword: "NEW_PASSWORD",
}, },
// Scenario 2: Happy Path, but without redirect. // Scenario 2: Happy Path, but without redirect.
{ { // Case 2
// Step 2.1 - User clicks link in email, has valid token. // Step 2.1 - User clicks link in email, has valid token.
query: url.Values{ query: url.Values{
"token": str(makeToken("ID-1", "password", url.URL{}, time.Hour*1, goodSigner)), "token": str(makeToken("ID-1", "password", testClientID, url.URL{}, time.Hour*1, goodSigner)),
}, },
method: "GET", method: "GET",
wantCode: http.StatusOK, wantCode: http.StatusOK,
wantFormValues: &url.Values{ wantFormValues: &url.Values{
"password": str(""), "password": str(""),
"token": str(makeToken("ID-1", "password", url.URL{}, time.Hour*1, goodSigner)), "token": str(makeToken("ID-1", "password", testClientID, url.URL{}, time.Hour*1, goodSigner)),
}, },
wantPassword: "password", wantPassword: "password",
}, },
{ { // Case 3
// Step 2.2 - User enters in new valid password, password is changed, user is redirected. // Step 2.2 - User enters in new valid password, password is changed, user is redirected.
query: url.Values{ query: url.Values{
"token": str(makeToken("ID-1", "password", url.URL{}, time.Hour*1, goodSigner)), "token": str(makeToken("ID-1", "password", testClientID, url.URL{}, time.Hour*1, goodSigner)),
"password": str("new_password"), "password": str("new_password"),
}, },
method: "POST", method: "POST",
...@@ -454,10 +457,10 @@ func TestResetPasswordHandler(t *testing.T) { ...@@ -454,10 +457,10 @@ func TestResetPasswordHandler(t *testing.T) {
wantPassword: "NEW_PASSWORD", wantPassword: "NEW_PASSWORD",
}, },
// Errors // Errors
{ { // Case 4
// Step 1.1.1 - User clicks link in email, has invalid token. // Step 1.1.1 - User clicks link in email, has invalid token.
query: url.Values{ query: url.Values{
"token": str(makeToken("ID-1", "password", testRedirectURL, time.Hour*1, badSigner)), "token": str(makeToken("ID-1", "password", testClientID, testRedirectURL, time.Hour*1, badSigner)),
}, },
method: "GET", method: "GET",
...@@ -466,10 +469,10 @@ func TestResetPasswordHandler(t *testing.T) { ...@@ -466,10 +469,10 @@ func TestResetPasswordHandler(t *testing.T) {
wantPassword: "password", wantPassword: "password",
}, },
{ { // Case 5
// Step 2.2.1 - User enters in new valid password, password is changed, user is redirected. // Step 2.2.1 - User enters in new valid password, password is changed, no redirect
query: url.Values{ query: url.Values{
"token": str(makeToken("ID-1", "password", url.URL{}, time.Hour*1, goodSigner)), "token": str(makeToken("ID-1", "password", testClientID, url.URL{}, time.Hour*1, goodSigner)),
"password": str("shrt"), "password": str("shrt"),
}, },
method: "POST", method: "POST",
...@@ -478,14 +481,14 @@ func TestResetPasswordHandler(t *testing.T) { ...@@ -478,14 +481,14 @@ func TestResetPasswordHandler(t *testing.T) {
wantCode: http.StatusBadRequest, wantCode: http.StatusBadRequest,
wantFormValues: &url.Values{ wantFormValues: &url.Values{
"password": str(""), "password": str(""),
"token": str(makeToken("ID-1", "password", url.URL{}, time.Hour*1, goodSigner)), "token": str(makeToken("ID-1", "password", testClientID, url.URL{}, time.Hour*1, goodSigner)),
}, },
wantPassword: "password", wantPassword: "password",
}, },
{ { // Case 6
// Step 2.2.2 - User enters in new valid password, with suspicious token. // Step 2.2.2 - User enters in new valid password, with suspicious token.
query: url.Values{ query: url.Values{
"token": str(makeToken("ID-1", "password", url.URL{}, time.Hour*1, badSigner)), "token": str(makeToken("ID-1", "password", testClientID, url.URL{}, time.Hour*1, badSigner)),
"password": str("shrt"), "password": str("shrt"),
}, },
method: "POST", method: "POST",
...@@ -495,6 +498,28 @@ func TestResetPasswordHandler(t *testing.T) { ...@@ -495,6 +498,28 @@ func TestResetPasswordHandler(t *testing.T) {
wantFormValues: &url.Values{}, wantFormValues: &url.Values{},
wantPassword: "password", wantPassword: "password",
}, },
{ // Case 7
// Token lacking client id
query: url.Values{
"token": str(makeToken("ID-1", "password", "", url.URL{}, time.Hour*1, goodSigner)),
"password": str("shrt"),
},
method: "GET",
wantCode: http.StatusBadRequest,
wantPassword: "password",
},
{ // Case 8
// Token lacking client id
query: url.Values{
"token": str(makeToken("ID-1", "password", "", url.URL{}, time.Hour*1, goodSigner)),
"password": str("shrt"),
},
method: "POST",
wantCode: http.StatusBadRequest,
wantPassword: "password",
},
} }
for i, tt := range tests { for i, tt := range tests {
f, err := makeTestFixtures() f, err := makeTestFixtures()
......
...@@ -14,7 +14,7 @@ COVER=${COVER:-"-cover"} ...@@ -14,7 +14,7 @@ COVER=${COVER:-"-cover"}
source ./build source ./build
TESTABLE="connector db integration pkg/crypto pkg/flag pkg/http pkg/net pkg/time pkg/html functional/repo server session user/api" TESTABLE="connector db integration pkg/crypto pkg/flag pkg/http pkg/net pkg/time pkg/html functional/repo server session user user/api"
FORMATTABLE="$TESTABLE cmd/dexctl cmd/dex-worker cmd/dex-overlord examples/app functional pkg/log" FORMATTABLE="$TESTABLE cmd/dexctl cmd/dex-worker cmd/dex-overlord examples/app functional pkg/log"
# user has not provided PKG override # user has not provided PKG override
......
...@@ -76,18 +76,19 @@ func (u *UserEmailer) SendResetPasswordEmail(email string, redirectURL url.URL, ...@@ -76,18 +76,19 @@ func (u *UserEmailer) SendResetPasswordEmail(email string, redirectURL url.URL,
} }
signer, err := u.signerFn() signer, err := u.signerFn()
if err != nil { if err != nil || signer == nil {
log.Errorf("error getting signer: %v", err) log.Errorf("error getting signer: %v (%v)", err, signer)
return nil, err return nil, err
} }
passwordReset := user.NewPasswordReset(usr, pwi.Password, u.issuerURL, passwordReset := user.NewPasswordReset(usr, pwi.Password, u.issuerURL,
clientID, redirectURL, u.tokenValidityWindow) clientID, redirectURL, u.tokenValidityWindow)
token, err := passwordReset.Token(signer) jwt, err := jose.NewSignedJWT(passwordReset.Claims, signer)
if err != nil { if err != nil {
log.Errorf("error getting tokenizing PasswordReset: %v", err) log.Errorf("error constructing or signing PasswordReset JWT: %v", err)
return nil, err return nil, err
} }
token := jwt.Encode()
resetURL := u.passwordResetURL resetURL := u.passwordResetURL
q := resetURL.Query() q := resetURL.Query()
...@@ -124,15 +125,17 @@ func (u *UserEmailer) SendEmailVerification(userID, clientID string, redirectURL ...@@ -124,15 +125,17 @@ func (u *UserEmailer) SendEmailVerification(userID, clientID string, redirectURL
ev := user.NewEmailVerification(usr, clientID, u.issuerURL, redirectURL, u.tokenValidityWindow) ev := user.NewEmailVerification(usr, clientID, u.issuerURL, redirectURL, u.tokenValidityWindow)
signer, err := u.signerFn() signer, err := u.signerFn()
if err != nil { if err != nil || signer == nil {
log.Errorf("error getting signer: %v", err) log.Errorf("error getting signer: %v (signer: %v)", err, signer)
return nil, err return nil, err
} }
token, err := ev.Token(signer) jwt, err := jose.NewSignedJWT(ev.Claims, signer)
if err != nil { if err != nil {
log.Errorf("error constructing or signing EmailVerification JWT: %v", err)
return nil, err return nil, err
} }
token := jwt.Encode()
verifyURL := u.verifyEmailURL verifyURL := u.verifyEmailURL
q := verifyURL.Query() q := verifyURL.Query()
......
package user package user
import ( import (
"errors"
"fmt" "fmt"
"net/url" "net/url"
"time" "time"
...@@ -13,15 +12,6 @@ import ( ...@@ -13,15 +12,6 @@ import (
"github.com/coreos/go-oidc/oidc" "github.com/coreos/go-oidc/oidc"
) )
const (
// Claim representing where a user should be sent after verifying their email address.
ClaimEmailVerificationCallback = "http://coreos.com/email/verification-callback"
// ClaimEmailVerificationEmail represents the email to be verified. Note
// that we are intentionally not using the "email" claim for this purpose.
ClaimEmailVerificationEmail = "http://coreos.com/email/verificationEmail"
)
var ( var (
clock = clockwork.NewRealClock() clock = clockwork.NewRealClock()
) )
...@@ -29,7 +19,6 @@ var ( ...@@ -29,7 +19,6 @@ var (
// NewEmailVerification creates an object which can be sent to a user in serialized form to verify that they control an email address. // NewEmailVerification creates an object which can be sent to a user in serialized form to verify that they control an email address.
// The clientID is the ID of the registering user. The callback is where a user should land after verifying their email. // The clientID is the ID of the registering user. The callback is where a user should land after verifying their email.
func NewEmailVerification(user User, clientID string, issuer url.URL, callback url.URL, expires time.Duration) EmailVerification { func NewEmailVerification(user User, clientID string, issuer url.URL, callback url.URL, expires time.Duration) EmailVerification {
claims := oidc.NewClaims(issuer.String(), user.ID, clientID, clock.Now(), clock.Now().Add(expires)) claims := oidc.NewClaims(issuer.String(), user.ID, clientID, clock.Now(), clock.Now().Add(expires))
claims.Add(ClaimEmailVerificationCallback, callback.String()) claims.Add(ClaimEmailVerificationCallback, callback.String())
claims.Add(ClaimEmailVerificationEmail, user.Email) claims.Add(ClaimEmailVerificationEmail, user.Email)
...@@ -37,90 +26,46 @@ func NewEmailVerification(user User, clientID string, issuer url.URL, callback u ...@@ -37,90 +26,46 @@ func NewEmailVerification(user User, clientID string, issuer url.URL, callback u
} }
type EmailVerification struct { type EmailVerification struct {
claims jose.Claims Claims jose.Claims
}
// Token serializes the EmailVerification into a signed JWT.
func (e EmailVerification) Token(signer jose.Signer) (string, error) {
if signer == nil {
return "", errors.New("no signer")
}
jwt, err := jose.NewSignedJWT(e.claims, signer)
if err != nil {
return "", err
}
return jwt.Encode(), nil
} }
// ParseAndVerifyEmailVerificationToken parses a string into a an EmailVerification, verifies the signature, and ensures that required claims are present. // Assumes that parseAndVerifyTokenClaims has already been called on claims
// In addition to the usual claims required by the OIDC spec, "aud" and "sub" must be present as well as ClaimEmailVerificationCallback and ClaimEmailVerificationEmail. func verifyEmailVerificationClaims(claims jose.Claims) (EmailVerification, error) {
func ParseAndVerifyEmailVerificationToken(token string, issuer url.URL, keys []key.PublicKey) (EmailVerification, error) { email, ok, err := claims.StringClaim(ClaimEmailVerificationEmail)
jwt, err := jose.ParseJWT(token)
if err != nil {
return EmailVerification{}, err
}
claims, err := jwt.Claims()
if err != nil {
return EmailVerification{}, err
}
clientID, ok, err := claims.StringClaim("aud")
if err != nil { if err != nil {
return EmailVerification{}, err return EmailVerification{}, err
} }
if !ok { if !ok || email == "" {
return EmailVerification{}, errors.New("no aud(client ID) claim") return EmailVerification{}, fmt.Errorf("no %q claim", ClaimEmailVerificationEmail)
} }
cb, ok, err := claims.StringClaim(ClaimEmailVerificationCallback) cb, ok, err := claims.StringClaim(ClaimEmailVerificationCallback)
if err != nil { if err != nil {
return EmailVerification{}, err return EmailVerification{}, err
} }
if cb == "" { if !ok || cb == "" {
return EmailVerification{}, fmt.Errorf("no %q claim", ClaimEmailVerificationCallback) return EmailVerification{}, fmt.Errorf("no %q claim", ClaimEmailVerificationCallback)
} }
if _, err := url.Parse(cb); err != nil { if _, err := url.Parse(cb); err != nil {
return EmailVerification{}, fmt.Errorf("callback URL not parseable: %v", cb) return EmailVerification{}, fmt.Errorf("callback URL not parseable: %v", cb)
} }
email, ok, err := claims.StringClaim(ClaimEmailVerificationEmail) return EmailVerification{claims}, nil
if err != nil { }
return EmailVerification{}, err
}
if email == "" {
return EmailVerification{}, fmt.Errorf("no %q claim", ClaimEmailVerificationEmail)
}
sub, ok, err := claims.StringClaim("sub") // ParseAndVerifyEmailVerificationToken parses a string into a an EmailVerification, verifies the signature, and ensures that required claims are present.
// In addition to the usual claims required by the OIDC spec, "aud" and "sub" must be present as well as ClaimEmailVerificationCallback and ClaimEmailVerificationEmail.
func ParseAndVerifyEmailVerificationToken(token string, issuer url.URL, keys []key.PublicKey) (EmailVerification, error) {
tokenClaims, err := parseAndVerifyTokenClaims(token, issuer, keys)
if err != nil { if err != nil {
return EmailVerification{}, err return EmailVerification{}, err
} }
if sub == "" {
return EmailVerification{}, errors.New("no sub claim")
}
noop := func() error { return nil }
keysFunc := func() []key.PublicKey {
return keys
}
verifier := oidc.NewJWTVerifier(issuer.String(), clientID, noop, keysFunc)
if err := verifier.Verify(jwt); err != nil {
return EmailVerification{}, err
}
return EmailVerification{
claims: claims,
}, nil
return verifyEmailVerificationClaims(tokenClaims.Claims)
} }
func (e EmailVerification) UserID() string { func (e EmailVerification) UserID() string {
uid, ok, err := e.claims.StringClaim("sub") uid, ok, err := e.Claims.StringClaim("sub")
if !ok || err != nil { if !ok || err != nil {
panic("EmailVerification: no sub claim. This should be impossible.") panic("EmailVerification: no sub claim. This should be impossible.")
} }
...@@ -128,7 +73,7 @@ func (e EmailVerification) UserID() string { ...@@ -128,7 +73,7 @@ func (e EmailVerification) UserID() string {
} }
func (e EmailVerification) Email() string { func (e EmailVerification) Email() string {
email, ok, err := e.claims.StringClaim(ClaimEmailVerificationEmail) email, ok, err := e.Claims.StringClaim(ClaimEmailVerificationEmail)
if !ok || err != nil { if !ok || err != nil {
panic("EmailVerification: no email claim. This should be impossible.") panic("EmailVerification: no email claim. This should be impossible.")
} }
...@@ -136,7 +81,7 @@ func (e EmailVerification) Email() string { ...@@ -136,7 +81,7 @@ func (e EmailVerification) Email() string {
} }
func (e EmailVerification) Callback() *url.URL { func (e EmailVerification) Callback() *url.URL {
cb, ok, err := e.claims.StringClaim(ClaimEmailVerificationCallback) cb, ok, err := e.Claims.StringClaim(ClaimEmailVerificationCallback)
if !ok || err != nil { if !ok || err != nil {
panic("EmailVerification: no callback claim. This should be impossible.") panic("EmailVerification: no callback claim. This should be impossible.")
} }
......
...@@ -59,7 +59,7 @@ func TestNewEmailVerification(t *testing.T) { ...@@ -59,7 +59,7 @@ func TestNewEmailVerification(t *testing.T) {
} }
ev := NewEmailVerification(tt.user, tt.clientID, tt.issuer, *cbURL, tt.expires) ev := NewEmailVerification(tt.user, tt.clientID, tt.issuer, *cbURL, tt.expires)
if diff := pretty.Compare(tt.want, ev.claims); diff != "" { if diff := pretty.Compare(tt.want, ev.Claims); diff != "" {
t.Errorf("case %d: Compare(want, got): %v", i, diff) t.Errorf("case %d: Compare(want, got): %v", i, diff)
} }
...@@ -127,10 +127,11 @@ func TestEmailVerificationParseAndVerify(t *testing.T) { ...@@ -127,10 +127,11 @@ func TestEmailVerificationParseAndVerify(t *testing.T) {
for i, tt := range tests { for i, tt := range tests {
token, err := tt.ev.Token(tt.signer) jwt, err := jose.NewSignedJWT(tt.ev.Claims, tt.signer)
if err != nil { if err != nil {
t.Errorf("case %d: non-nil error creating token: %v", i, err) t.Fatalf("Failed to generate JWT, error=%v", err)
} }
token := jwt.Encode()
ev, err := ParseAndVerifyEmailVerificationToken(token, *issuer, ev, err := ParseAndVerifyEmailVerificationToken(token, *issuer,
[]key.PublicKey{*key.NewPublicKey(privKey.JWK())}) []key.PublicKey{*key.NewPublicKey(privKey.JWK())})
...@@ -148,7 +149,7 @@ func TestEmailVerificationParseAndVerify(t *testing.T) { ...@@ -148,7 +149,7 @@ func TestEmailVerificationParseAndVerify(t *testing.T) {
} }
if diff := pretty.Compare(tt.ev.claims, ev.claims); diff != "" { if diff := pretty.Compare(tt.ev.Claims, ev.Claims); diff != "" {
t.Errorf("case %d: Compare(want, got): %v", i, diff) t.Errorf("case %d: Compare(want, got): %v", i, diff)
} }
} }
......
...@@ -26,14 +26,6 @@ const ( ...@@ -26,14 +26,6 @@ const (
// since the bcrypt library will silently ignore portions of // since the bcrypt library will silently ignore portions of
// a password past the first 72 characters. // a password past the first 72 characters.
maxSecretLength = 72 maxSecretLength = 72
// ClaimPasswordResetCallback represents where a user should be sent after
// resetting their password.
ClaimPasswordResetCallback = "http://coreos.com/password/reset-callback"
// ClaimPasswordResetPassword represents the hash of the password to be
// reset; in other words, the old password.
ClaimPasswordResetPassword = "http://coreos.com/password/old-hash"
) )
var ( var (
...@@ -224,56 +216,22 @@ func NewPasswordInfoRepoFromFile(loc string) (PasswordInfoRepo, error) { ...@@ -224,56 +216,22 @@ func NewPasswordInfoRepoFromFile(loc string) (PasswordInfoRepo, error) {
func NewPasswordReset(user User, password Password, issuer url.URL, clientID string, callback url.URL, expires time.Duration) PasswordReset { func NewPasswordReset(user User, password Password, issuer url.URL, clientID string, callback url.URL, expires time.Duration) PasswordReset {
claims := oidc.NewClaims(issuer.String(), user.ID, clientID, clock.Now(), clock.Now().Add(expires)) claims := oidc.NewClaims(issuer.String(), user.ID, clientID, clock.Now(), clock.Now().Add(expires))
claims.Add(ClaimPasswordResetCallback, callback.String())
claims.Add(ClaimPasswordResetPassword, string(password)) claims.Add(ClaimPasswordResetPassword, string(password))
claims.Add(ClaimPasswordResetCallback, callback.String())
return PasswordReset{claims} return PasswordReset{claims}
} }
type PasswordReset struct { type PasswordReset struct {
claims jose.Claims Claims jose.Claims
}
// Token serializes the PasswordReset into a signed JWT.
func (e PasswordReset) Token(signer jose.Signer) (string, error) {
if signer == nil {
return "", errors.New("no signer")
}
jwt, err := jose.NewSignedJWT(e.claims, signer)
if err != nil {
return "", err
}
return jwt.Encode(), nil
} }
// ParseAndVerifyPasswordResetToken parses a string into a an PasswordReset, verifies the signature, and ensures that required claims are present. // Assumes that parseAndVerifyTokenClaims has already been called on claims
// In addition to the usual claims required by the OIDC spec, "aud" and "sub" must be present as well as ClaimPasswordResetCallback, ClaimPasswordResetEmail and ClaimPasswordResetPassword. func verifyPasswordResetClaims(claims jose.Claims) (PasswordReset, error) {
func ParseAndVerifyPasswordResetToken(token string, issuer url.URL, keys []key.PublicKey) (PasswordReset, error) {
jwt, err := jose.ParseJWT(token)
if err != nil {
return PasswordReset{}, err
}
claims, err := jwt.Claims()
if err != nil {
return PasswordReset{}, err
}
cb, ok, err := claims.StringClaim(ClaimPasswordResetCallback) cb, ok, err := claims.StringClaim(ClaimPasswordResetCallback)
if err != nil { if err != nil {
return PasswordReset{}, err return PasswordReset{}, err
} }
var clientID string
if ok && cb != "" {
clientID, ok, err = claims.StringClaim("aud")
if err != nil {
return PasswordReset{}, err
}
if !ok || clientID == "" {
return PasswordReset{}, errors.New("no aud(client ID) claim")
}
}
if _, err := url.Parse(cb); err != nil { if _, err := url.Parse(cb); err != nil {
return PasswordReset{}, fmt.Errorf("callback URL not parseable: %v", cb) return PasswordReset{}, fmt.Errorf("callback URL not parseable: %v", cb)
} }
...@@ -282,37 +240,26 @@ func ParseAndVerifyPasswordResetToken(token string, issuer url.URL, keys []key.P ...@@ -282,37 +240,26 @@ func ParseAndVerifyPasswordResetToken(token string, issuer url.URL, keys []key.P
if err != nil { if err != nil {
return PasswordReset{}, err return PasswordReset{}, err
} }
if pw == "" { if !ok || pw == "" {
return PasswordReset{}, fmt.Errorf("no %q claim", ClaimPasswordResetPassword) return PasswordReset{}, fmt.Errorf("no %q claim", ClaimPasswordResetPassword)
} }
sub, ok, err := claims.StringClaim("sub") return PasswordReset{claims}, nil
if err != nil { }
return PasswordReset{}, err
}
if sub == "" {
return PasswordReset{}, errors.New("no sub claim")
}
noop := func() error { return nil }
keysFunc := func() []key.PublicKey {
return keys
}
verifier := oidc.NewJWTVerifier(issuer.String(), clientID, noop, keysFunc) // ParseAndVerifyPasswordResetToken parses a string into a an PasswordReset, verifies the signature, and ensures that required claims are present.
if err := verifier.Verify(jwt); err != nil { // In addition to the usual claims required by the OIDC spec, "aud" and "sub" must be present as well as ClaimPasswordResetCallback, ClaimPasswordResetEmail and ClaimPasswordResetPassword.
func ParseAndVerifyPasswordResetToken(token string, issuer url.URL, keys []key.PublicKey) (PasswordReset, error) {
tokenClaims, err := parseAndVerifyTokenClaims(token, issuer, keys)
if err != nil {
return PasswordReset{}, err return PasswordReset{}, err
} }
return PasswordReset{ return verifyPasswordResetClaims(tokenClaims.Claims)
claims: claims,
}, nil
} }
func (e PasswordReset) UserID() string { func (e PasswordReset) UserID() string {
uid, ok, err := e.claims.StringClaim("sub") uid, ok, err := e.Claims.StringClaim("sub")
if !ok || err != nil { if !ok || err != nil {
panic("PasswordReset: no sub claim. This should be impossible.") panic("PasswordReset: no sub claim. This should be impossible.")
} }
...@@ -320,7 +267,7 @@ func (e PasswordReset) UserID() string { ...@@ -320,7 +267,7 @@ func (e PasswordReset) UserID() string {
} }
func (e PasswordReset) Password() Password { func (e PasswordReset) Password() Password {
pw, ok, err := e.claims.StringClaim(ClaimPasswordResetPassword) pw, ok, err := e.Claims.StringClaim(ClaimPasswordResetPassword)
if !ok || err != nil { if !ok || err != nil {
panic("PasswordReset: no password claim. This should be impossible.") panic("PasswordReset: no password claim. This should be impossible.")
} }
...@@ -328,7 +275,7 @@ func (e PasswordReset) Password() Password { ...@@ -328,7 +275,7 @@ func (e PasswordReset) Password() Password {
} }
func (e PasswordReset) Callback() *url.URL { func (e PasswordReset) Callback() *url.URL {
cb, ok, err := e.claims.StringClaim(ClaimPasswordResetCallback) cb, ok, err := e.Claims.StringClaim(ClaimPasswordResetCallback)
if err != nil { if err != nil {
panic("PasswordReset: error getting string claim. This should be impossible.") panic("PasswordReset: error getting string claim. This should be impossible.")
} }
......
...@@ -124,7 +124,7 @@ func TestNewPasswordReset(t *testing.T) { ...@@ -124,7 +124,7 @@ func TestNewPasswordReset(t *testing.T) {
} }
ev := NewPasswordReset(tt.user, tt.password, tt.issuer, tt.clientID, *cbURL, tt.expires) ev := NewPasswordReset(tt.user, tt.password, tt.issuer, tt.clientID, *cbURL, tt.expires)
if diff := pretty.Compare(tt.want, ev.claims); diff != "" { if diff := pretty.Compare(tt.want, ev.Claims); diff != "" {
t.Errorf("case %d: Compare(want, got): %v", i, diff) t.Errorf("case %d: Compare(want, got): %v", i, diff)
} }
...@@ -145,12 +145,13 @@ func TestPasswordResetParseAndVerify(t *testing.T) { ...@@ -145,12 +145,13 @@ func TestPasswordResetParseAndVerify(t *testing.T) {
password := Password("passy") password := Password("passy")
goodPR := NewPasswordReset(user, password, *issuer, client, *callback, expires) goodPR := NewPasswordReset(user, password, *issuer, client, *callback, expires)
goodPRNoCB := NewPasswordReset(user, password, *issuer, "", url.URL{}, expires) goodPRNoCB := NewPasswordReset(user, password, *issuer, client, url.URL{}, expires)
expiredPR := NewPasswordReset(user, password, *issuer, client, *callback, -expires) expiredPR := NewPasswordReset(user, password, *issuer, client, *callback, -expires)
wrongIssuerPR := NewPasswordReset(user, password, *otherIssuer, client, *callback, expires) wrongIssuerPR := NewPasswordReset(user, password, *otherIssuer, client, *callback, expires)
noSubPR := NewPasswordReset(User{}, password, *issuer, client, *callback, expires) noSubPR := NewPasswordReset(User{}, password, *issuer, client, *callback, expires)
noPWPR := NewPasswordReset(user, Password(""), *issuer, client, *callback, expires) noPWPR := NewPasswordReset(user, Password(""), *issuer, client, *callback, expires)
noClientPR := NewPasswordReset(user, password, *issuer, "", *callback, expires) noClientPR := NewPasswordReset(user, password, *issuer, "", *callback, expires)
noClientNoCBPR := NewPasswordReset(user, password, *issuer, "", url.URL{}, expires)
privKey, err := key.GeneratePrivateKey() privKey, err := key.GeneratePrivateKey()
if err != nil { if err != nil {
...@@ -211,14 +212,20 @@ func TestPasswordResetParseAndVerify(t *testing.T) { ...@@ -211,14 +212,20 @@ func TestPasswordResetParseAndVerify(t *testing.T) {
signer: signer, signer: signer,
wantErr: true, wantErr: true,
}, },
{
ev: noClientNoCBPR,
signer: signer,
wantErr: true,
},
} }
for i, tt := range tests { for i, tt := range tests {
token, err := tt.ev.Token(tt.signer) jwt, err := jose.NewSignedJWT(tt.ev.Claims, tt.signer)
if err != nil { if err != nil {
t.Errorf("case %d: non-nil error creating token: %v", i, err) t.Fatalf("Failed to generate JWT, error=%v", err)
} }
token := jwt.Encode()
ev, err := ParseAndVerifyPasswordResetToken(token, *issuer, ev, err := ParseAndVerifyPasswordResetToken(token, *issuer,
[]key.PublicKey{*key.NewPublicKey(privKey.JWK())}) []key.PublicKey{*key.NewPublicKey(privKey.JWK())})
...@@ -236,7 +243,7 @@ func TestPasswordResetParseAndVerify(t *testing.T) { ...@@ -236,7 +243,7 @@ func TestPasswordResetParseAndVerify(t *testing.T) {
} }
if diff := pretty.Compare(tt.ev.claims, ev.claims); diff != "" { if diff := pretty.Compare(tt.ev.Claims, ev.Claims); diff != "" {
t.Errorf("case %d: Compare(want, got): %v", i, diff) t.Errorf("case %d: Compare(want, got): %v", i, diff)
} }
} }
......
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"time" "time"
"net/mail" "net/mail"
"net/url"
"os" "os"
"sort" "sort"
...@@ -15,10 +16,30 @@ import ( ...@@ -15,10 +16,30 @@ import (
"github.com/coreos/dex/repo" "github.com/coreos/dex/repo"
"github.com/coreos/go-oidc/jose" "github.com/coreos/go-oidc/jose"
"github.com/coreos/go-oidc/key"
"github.com/coreos/go-oidc/oidc"
) )
const ( const (
MaxEmailLength = 200 MaxEmailLength = 200
// ClaimPasswordResetPassword represents the hash of the password to be
// reset; in other words, the old password
ClaimPasswordResetPassword = "http://coreos.com/password/old-hash"
// ClaimEmailVerificationEmail represents the email to be verified. Note
// that we are intentionally not using the "email" claim for this purpose.
ClaimEmailVerificationEmail = "http://coreos.com/email/verificationEmail"
// ClaimPasswordResetCallback represents where a user should be sent after
// resetting their password.
ClaimPasswordResetCallback = "http://coreos.com/password/reset-callback"
// Claim representing where a user should be sent after verifying their email address.
ClaimEmailVerificationCallback = "http://coreos.com/email/verification-callback"
// Claim representing where a user should be sent after responding to an invitation
ClaimInvitationCallback = "http://coreos.com/invitation/callback"
) )
type UserIDGenerator func() (string, error) type UserIDGenerator func() (string, error)
...@@ -422,3 +443,53 @@ func (u *RemoteIdentity) UnmarshalJSON(data []byte) error { ...@@ -422,3 +443,53 @@ func (u *RemoteIdentity) UnmarshalJSON(data []byte) error {
return nil return nil
} }
type TokenClaims struct {
Claims jose.Claims
}
// Returns TokenClaims if and only if
// - the given token string is an appropriately formatted JWT
// - the JWT contains nonempty "aud" and "sub" claims
// - the JWT can be verified for the client associated with the "aud" claim
// using the given keys
func parseAndVerifyTokenClaims(token string, issuer url.URL, keys []key.PublicKey) (TokenClaims, error) {
jwt, err := jose.ParseJWT(token)
if err != nil {
return TokenClaims{}, err
}
claims, err := jwt.Claims()
if err != nil {
return TokenClaims{}, err
}
clientID, ok, err := claims.StringClaim("aud")
if err != nil {
return TokenClaims{}, err
}
if !ok || clientID == "" {
return TokenClaims{}, errors.New("no aud(client ID) claim")
}
sub, ok, err := claims.StringClaim("sub")
if err != nil {
return TokenClaims{}, err
}
if !ok || sub == "" {
return TokenClaims{}, errors.New("no sub claim")
}
noop := func() error { return nil }
keysFunc := func() []key.PublicKey {
return keys
}
verifier := oidc.NewJWTVerifier(issuer.String(), clientID, noop, keysFunc)
if err := verifier.Verify(jwt); err != nil {
return TokenClaims{}, err
}
return TokenClaims{claims}, nil
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment