forked from mystiq/dex
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()
|
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 {
|
||||||
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
|
||||||
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
|
||||||
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 {
|
||||||
|
|
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
|
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 {
|
||||||
|
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 {
|
if err != nil {
|
||||||
return PasswordReset{}, err
|
return PasswordReset{}, err
|
||||||
}
|
}
|
||||||
|
@ -236,26 +253,7 @@ func verifyPasswordResetClaims(claims jose.Claims) (PasswordReset, error) {
|
||||||
return PasswordReset{}, fmt.Errorf("callback URL not parseable: %v", cb)
|
return PasswordReset{}, fmt.Errorf("callback URL not parseable: %v", cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
pw, ok, err := claims.StringClaim(ClaimPasswordResetPassword)
|
return PasswordReset{tokenClaims.Claims}, nil
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e PasswordReset) UserID() string {
|
func (e PasswordReset) UserID() string {
|
||||||
|
|
Loading…
Reference in a new issue