forked from mystiq/dex
Merge pull request #158 from joeatwork/share-token-code
server: Share token code
This commit is contained in:
commit
70eb87d87c
10 changed files with 268 additions and 249 deletions
4
build
4
build
|
@ -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
|
||||||
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) {
|
||||||
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) {
|
||||||
}.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) {
|
||||||
"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) {
|
||||||
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) {
|
||||||
},
|
},
|
||||||
|
|
||||||
// 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) {
|
||||||
"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) {
|
||||||
|
|
||||||
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(""),
|
{ // Case 8
|
||||||
"redirect_uri": str(""),
|
|
||||||
"email": str(""),
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
|
|
||||||
// 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(""),
|
{ // Case 9
|
||||||
"redirect_uri": str(""),
|
|
||||||
"email": str(""),
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
// 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) {
|
||||||
},
|
},
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
||||||
wantCode: http.StatusOK,
|
wantCode: http.StatusBadRequest,
|
||||||
wantEmailer: &testEmailer{
|
},
|
||||||
to: str("Email-1@example.com"),
|
{ // Case 10
|
||||||
from: "noreply@example.com",
|
|
||||||
subject: "Reset your password.",
|
// User hits the page with a valid email but no client id
|
||||||
|
query: url.Values{
|
||||||
|
"email": str("Email-1@example.com"),
|
||||||
},
|
},
|
||||||
wantPRPassword: "password",
|
method: "GET",
|
||||||
wantPRUserID: "ID-1",
|
|
||||||
wantPRRedirect: nil,
|
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) {
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
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) {
|
||||||
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) {
|
||||||
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) {
|
||||||
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) {
|
||||||
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) {
|
||||||
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()
|
||||||
|
|
2
test
2
test
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
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()
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
@ -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 (
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
type EmailVerification struct {
|
type EmailVerification struct {
|
||||||
claims jose.Claims
|
Claims jose.Claims
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token serializes the EmailVerification into a signed JWT.
|
// Assumes that parseAndVerifyTokenClaims has already been called on claims
|
||||||
func (e EmailVerification) Token(signer jose.Signer) (string, error) {
|
func verifyEmailVerificationClaims(claims jose.Claims) (EmailVerification, error) {
|
||||||
if signer == nil {
|
email, ok, err := claims.StringClaim(ClaimEmailVerificationEmail)
|
||||||
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.
|
|
||||||
// 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) {
|
|
||||||
jwt, err := jose.ParseJWT(token)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return EmailVerification{}, err
|
return EmailVerification{}, err
|
||||||
}
|
}
|
||||||
|
if !ok || email == "" {
|
||||||
claims, err := jwt.Claims()
|
return EmailVerification{}, fmt.Errorf("no %q claim", ClaimEmailVerificationEmail)
|
||||||
if err != nil {
|
|
||||||
return EmailVerification{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
clientID, ok, err := claims.StringClaim("aud")
|
|
||||||
if err != nil {
|
|
||||||
return EmailVerification{}, err
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return EmailVerification{}, errors.New("no aud(client ID) claim")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 email == "" {
|
|
||||||
return EmailVerification{}, fmt.Errorf("no %q claim", ClaimEmailVerificationEmail)
|
|
||||||
}
|
|
||||||
|
|
||||||
sub, ok, err := claims.StringClaim("sub")
|
|
||||||
if err != nil {
|
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
}
|
}
|
||||||
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) {
|
||||||
|
|
||||||
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) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
||||||
// 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) {
|
||||||
|
|
||||||
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.
|
// Assumes that parseAndVerifyTokenClaims has already been called on claims
|
||||||
func (e PasswordReset) Token(signer jose.Signer) (string, error) {
|
func verifyPasswordResetClaims(claims jose.Claims) (PasswordReset, 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.
|
|
||||||
// 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) {
|
|
||||||
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
|
||||||
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseAndVerifyPasswordResetToken parses a string into a an PasswordReset, 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 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 {
|
if err != nil {
|
||||||
return PasswordReset{}, err
|
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)
|
|
||||||
if err := verifier.Verify(jwt); err != nil {
|
|
||||||
return PasswordReset{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return PasswordReset{
|
|
||||||
claims: claims,
|
|
||||||
}, nil
|
|
||||||
|
|
||||||
|
return verifyPasswordResetClaims(tokenClaims.Claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
}
|
}
|
||||||
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) {
|
||||||
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) {
|
||||||
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) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
71
user/user.go
71
user/user.go
|
@ -8,6 +8,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue