user: claims and parsing for invitations
This commit is contained in:
parent
ca9227fc19
commit
468c1b8b5e
4 changed files with 211 additions and 41 deletions
|
@ -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
59
user/invitation.go
Normal 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
113
user/invitation_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
Reference in a new issue