user: claims and parsing for invitations

This commit is contained in:
Joe Bowers 2015-10-19 10:24:55 -07:00
parent ca9227fc19
commit 468c1b8b5e
4 changed files with 211 additions and 41 deletions

View file

@ -16,8 +16,10 @@ var (
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.
// The clientID is the ID of the registering user. The callback is where a user should land after verifying their email.
// NewEmailVerification creates an object which can be sent to a user
// 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 {
claims := oidc.NewClaims(issuer.String(), user.ID, clientID, clock.Now(), clock.Now().Add(expires))
claims.Add(ClaimEmailVerificationCallback, callback.String())
@ -29,9 +31,18 @@ type EmailVerification struct {
Claims jose.Claims
}
// Assumes that parseAndVerifyTokenClaims has already been called on claims
func verifyEmailVerificationClaims(claims jose.Claims) (EmailVerification, error) {
email, ok, err := claims.StringClaim(ClaimEmailVerificationEmail)
// 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
}
email, ok, err := tokenClaims.Claims.StringClaim(ClaimEmailVerificationEmail)
if err != nil {
return EmailVerification{}, err
}
@ -39,7 +50,7 @@ func verifyEmailVerificationClaims(claims jose.Claims) (EmailVerification, error
return EmailVerification{}, fmt.Errorf("no %q claim", ClaimEmailVerificationEmail)
}
cb, ok, err := claims.StringClaim(ClaimEmailVerificationCallback)
cb, ok, err := tokenClaims.Claims.StringClaim(ClaimEmailVerificationCallback)
if err != nil {
return EmailVerification{}, err
}
@ -50,18 +61,7 @@ func verifyEmailVerificationClaims(claims jose.Claims) (EmailVerification, error
return EmailVerification{}, fmt.Errorf("callback URL not parseable: %v", cb)
}
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 {
return EmailVerification{}, err
}
return verifyEmailVerificationClaims(tokenClaims.Claims)
return EmailVerification{tokenClaims.Claims}, nil
}
func (e EmailVerification) UserID() string {

59
user/invitation.go Normal file
View file

@ -0,0 +1,59 @@
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
}

113
user/invitation_test.go Normal file
View file

@ -0,0 +1,113 @@
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)
}
}
}

View file

@ -225,9 +225,26 @@ type PasswordReset struct {
Claims jose.Claims
}
// Assumes that parseAndVerifyTokenClaims has already been called on claims
func verifyPasswordResetClaims(claims jose.Claims) (PasswordReset, error) {
cb, ok, err := 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 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
}
pw, ok, err := tokenClaims.Claims.StringClaim(ClaimPasswordResetPassword)
if err != nil {
return PasswordReset{}, err
}
if !ok || pw == "" {
return PasswordReset{}, fmt.Errorf("no %q claim", ClaimPasswordResetPassword)
}
cb, ok, err := tokenClaims.Claims.StringClaim(ClaimPasswordResetCallback)
if err != nil {
return PasswordReset{}, err
}
@ -236,26 +253,7 @@ func verifyPasswordResetClaims(claims jose.Claims) (PasswordReset, error) {
return PasswordReset{}, fmt.Errorf("callback URL not parseable: %v", cb)
}
pw, ok, err := claims.StringClaim(ClaimPasswordResetPassword)
if err != nil {
return PasswordReset{}, err
}
if !ok || pw == "" {
return PasswordReset{}, fmt.Errorf("no %q claim", ClaimPasswordResetPassword)
}
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 {
return PasswordReset{}, err
}
return verifyPasswordResetClaims(tokenClaims.Claims)
return PasswordReset{tokenClaims.Claims}, nil
}
func (e PasswordReset) UserID() string {