Commit 468c1b8b authored by Joe Bowers's avatar Joe Bowers

user: claims and parsing for invitations

parent ca9227fc
...@@ -16,8 +16,10 @@ var ( ...@@ -16,8 +16,10 @@ var (
clock = clockwork.NewRealClock() clock = clockwork.NewRealClock()
) )
// 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
// The clientID is the ID of the registering user. The callback is where a user should land after verifying their email. // in serialized form to verify that they control an email addwress.
// 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())
...@@ -29,9 +31,18 @@ type EmailVerification struct { ...@@ -29,9 +31,18 @@ type EmailVerification struct {
Claims jose.Claims Claims jose.Claims
} }
// Assumes that parseAndVerifyTokenClaims has already been called on claims // ParseAndVerifyEmailVerificationToken parses a string into a an
func verifyEmailVerificationClaims(claims jose.Claims) (EmailVerification, error) { // EmailVerification, verifies the signature, and ensures that
email, ok, err := claims.StringClaim(ClaimEmailVerificationEmail) // 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 {
return EmailVerification{}, err
}
email, ok, err := tokenClaims.Claims.StringClaim(ClaimEmailVerificationEmail)
if err != nil { if err != nil {
return EmailVerification{}, err return EmailVerification{}, err
} }
...@@ -39,7 +50,7 @@ func verifyEmailVerificationClaims(claims jose.Claims) (EmailVerification, error ...@@ -39,7 +50,7 @@ func verifyEmailVerificationClaims(claims jose.Claims) (EmailVerification, error
return EmailVerification{}, fmt.Errorf("no %q claim", ClaimEmailVerificationEmail) return EmailVerification{}, fmt.Errorf("no %q claim", ClaimEmailVerificationEmail)
} }
cb, ok, err := claims.StringClaim(ClaimEmailVerificationCallback) cb, ok, err := tokenClaims.Claims.StringClaim(ClaimEmailVerificationCallback)
if err != nil { if err != nil {
return EmailVerification{}, err return EmailVerification{}, err
} }
...@@ -50,18 +61,7 @@ func verifyEmailVerificationClaims(claims jose.Claims) (EmailVerification, error ...@@ -50,18 +61,7 @@ func verifyEmailVerificationClaims(claims jose.Claims) (EmailVerification, error
return EmailVerification{}, fmt.Errorf("callback URL not parseable: %v", cb) return EmailVerification{}, fmt.Errorf("callback URL not parseable: %v", cb)
} }
return EmailVerification{claims}, nil return EmailVerification{tokenClaims.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 {
return EmailVerification{}, err
}
return verifyEmailVerificationClaims(tokenClaims.Claims)
} }
func (e EmailVerification) UserID() string { func (e EmailVerification) UserID() string {
......
package user
import (
"fmt"
"net/url"
"time"
"github.com/coreos/go-oidc/jose"
"github.com/coreos/go-oidc/key"
"github.com/coreos/go-oidc/oidc"
)
func NewInvitation(user User, password Password, issuer url.URL, clientID string, callback url.URL, expires time.Duration) Invitation {
claims := oidc.NewClaims(issuer.String(), user.ID, clientID, clock.Now(), clock.Now().Add(expires))
claims.Add(ClaimPasswordResetPassword, string(password))
claims.Add(ClaimEmailVerificationEmail, user.Email)
claims.Add(ClaimInvitationCallback, callback.String())
return Invitation{claims}
}
type Invitation struct {
Claims jose.Claims
}
func ParseAndVerifyInvitationToken(token string, issuer url.URL, keys []key.PublicKey) (Invitation, error) {
tokenClaims, err := parseAndVerifyTokenClaims(token, issuer, keys)
if err != nil {
return Invitation{}, err
}
cb, ok, err := tokenClaims.Claims.StringClaim(ClaimInvitationCallback)
if err != nil {
return Invitation{}, err
}
if !ok || cb == "" {
return Invitation{}, fmt.Errorf("no %q claim", ClaimInvitationCallback)
}
if _, err := url.Parse(cb); err != nil {
return Invitation{}, fmt.Errorf("callback URL not parseable: %v", cb)
}
pw, ok, err := tokenClaims.Claims.StringClaim(ClaimPasswordResetPassword)
if err != nil {
return Invitation{}, err
}
if !ok || pw == "" {
return Invitation{}, fmt.Errorf("no %q claim", ClaimPasswordResetPassword)
}
email, ok, err := tokenClaims.Claims.StringClaim(ClaimEmailVerificationEmail)
if err != nil {
return Invitation{}, err
}
if !ok || email == "" {
return Invitation{}, fmt.Errorf("no %q claim", ClaimEmailVerificationEmail)
}
return Invitation{tokenClaims.Claims}, nil
}
package user
import (
"net/url"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/coreos/go-oidc/jose"
"github.com/coreos/go-oidc/key"
)
func TestInvitationParseAndVerify(t *testing.T) {
issuer, _ := url.Parse("http://example.com")
notIssuer, _ := url.Parse("http://other.com")
client := "myclient"
user := User{ID: "1234", Email: "user@example.com"}
callback, _ := url.Parse("http://client.example.com")
expires := time.Hour * 3
password := Password("Halloween is the best holiday")
privKey, _ := key.GeneratePrivateKey()
signer := privKey.Signer()
publicKeys := []key.PublicKey{*key.NewPublicKey(privKey.JWK())}
goodInvitation := NewInvitation(user, password, *issuer, client, *callback, expires)
goodNoCB := NewInvitation(user, password, *issuer, client, *callback, expires)
expired := NewInvitation(user, password, *issuer, client, *callback, -expires)
wrongIssuer := NewInvitation(user, password, *notIssuer, client, *callback, expires)
noSub := NewInvitation(User{Email: "noid@noid.com"}, password, *issuer, client, *callback, expires)
noEmail := NewInvitation(User{ID: "JONNY_NO_EMAIL"}, password, *issuer, client, *callback, expires)
noPassword := NewInvitation(user, Password(""), *issuer, client, *callback, expires)
noClient := NewInvitation(user, password, *issuer, "", *callback, expires)
noClientNoCB := NewInvitation(user, password, *issuer, "", url.URL{}, expires)
tests := []struct {
invite Invitation
wantErr bool
signer jose.Signer
}{
{
invite: goodInvitation,
signer: signer,
wantErr: false,
},
{
invite: goodNoCB,
signer: signer,
wantErr: false,
},
{
invite: expired,
signer: signer,
wantErr: true,
},
{
invite: wrongIssuer,
signer: signer,
wantErr: true,
},
{
invite: noSub,
signer: signer,
wantErr: true,
},
{
invite: noEmail,
signer: signer,
wantErr: true,
},
{
invite: noPassword,
signer: signer,
wantErr: true,
},
{
invite: noClient,
signer: signer,
wantErr: true,
},
{
invite: noClientNoCB,
signer: signer,
wantErr: true,
},
}
for i, tt := range tests {
jwt, err := jose.NewSignedJWT(tt.invite.Claims, tt.signer)
if err != nil {
t.Fatalf("case %d: failed to generate JWT, error: %v", i, err)
}
token := jwt.Encode()
parsed, err := ParseAndVerifyInvitationToken(token, *issuer, publicKeys)
if tt.wantErr {
if err == nil {
t.Errorf("case %d: want no-nil error, got nil", i)
}
continue
}
if err != nil {
t.Errorf("case %d: unexpected error: %v", i, err)
continue
}
if diff := pretty.Compare(tt.invite, parsed); diff != "" {
t.Errorf("case %d: Compare(want, got): %v", i, diff)
}
}
}
...@@ -225,18 +225,18 @@ type PasswordReset struct { ...@@ -225,18 +225,18 @@ type PasswordReset struct {
Claims jose.Claims Claims jose.Claims
} }
// Assumes that parseAndVerifyTokenClaims has already been called on claims // ParseAndVerifyPasswordResetToken parses a string into a an
func verifyPasswordResetClaims(claims jose.Claims) (PasswordReset, error) { // PasswordReset, verifies the signature, and ensures that required
cb, ok, err := claims.StringClaim(ClaimPasswordResetCallback) // claims are present. In addition to the usual claims required by
// the OIDC spec, "aud" and "sub" must be present as well as
// ClaimPasswordResetCallback 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 _, err := url.Parse(cb); err != nil { pw, ok, err := tokenClaims.Claims.StringClaim(ClaimPasswordResetPassword)
return PasswordReset{}, fmt.Errorf("callback URL not parseable: %v", cb)
}
pw, ok, err := claims.StringClaim(ClaimPasswordResetPassword)
if err != nil { if err != nil {
return PasswordReset{}, err return PasswordReset{}, err
} }
...@@ -244,18 +244,16 @@ func verifyPasswordResetClaims(claims jose.Claims) (PasswordReset, error) { ...@@ -244,18 +244,16 @@ func verifyPasswordResetClaims(claims jose.Claims) (PasswordReset, error) {
return PasswordReset{}, fmt.Errorf("no %q claim", ClaimPasswordResetPassword) return PasswordReset{}, fmt.Errorf("no %q claim", ClaimPasswordResetPassword)
} }
return PasswordReset{claims}, nil cb, ok, err := tokenClaims.Claims.StringClaim(ClaimPasswordResetCallback)
}
// 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
} }
return verifyPasswordResetClaims(tokenClaims.Claims) if _, err := url.Parse(cb); err != nil {
return PasswordReset{}, fmt.Errorf("callback URL not parseable: %v", cb)
}
return PasswordReset{tokenClaims.Claims}, nil
} }
func (e PasswordReset) UserID() string { func (e PasswordReset) UserID() string {
......
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