dex/user/password.go
Eric Chiang 221a1ad7a0 user: fix password info JSON encoding to survive round trips
PasswordInfos are marshaled when storing them in the database as
part of the local connector. However, the custom unmarsheler
defined could not unmarshal the standard marshling of this struct.

Add a struct tag to the Password field to correct this.

Closes #332
2016-02-23 16:25:56 -08:00

197 lines
5 KiB
Go

package user
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"time"
"golang.org/x/crypto/bcrypt"
"github.com/coreos/go-oidc/jose"
"github.com/coreos/go-oidc/key"
"github.com/coreos/go-oidc/oidc"
"github.com/coreos/dex/repo"
)
const (
bcryptHashCost = 10
// Blowfish, the algorithm underlying bcrypt, has a maximum
// password length of 72. We explicitly track and check this
// since the bcrypt library will silently ignore portions of
// a password past the first 72 characters.
maxSecretLength = 72
)
var (
PasswordHasher = DefaultPasswordHasher
ErrorInvalidPassword = errors.New("invalid Password")
ErrorPasswordHashNoMatch = errors.New("password and hash don't match")
ErrorPasswordExpired = errors.New("password has expired")
)
type Hasher func(string) ([]byte, error)
func DefaultPasswordHasher(s string) ([]byte, error) {
pwHash, err := bcrypt.GenerateFromPassword([]byte(s), bcryptHashCost)
if err != nil {
return nil, err
}
return Password(pwHash), nil
}
type Password []byte
func NewPasswordFromPlaintext(plaintext string) (Password, error) {
return PasswordHasher(plaintext)
}
type PasswordInfo struct {
UserID string
Password Password `json:"passwordHash"`
PasswordExpires time.Time `json:"passwordExpires"`
}
func (p PasswordInfo) Authenticate(plaintext string) (*oidc.Identity, error) {
if err := bcrypt.CompareHashAndPassword(p.Password, []byte(plaintext)); err != nil {
return nil, ErrorPasswordHashNoMatch
}
if !p.PasswordExpires.IsZero() && time.Now().After(p.PasswordExpires) {
return nil, ErrorPasswordExpired
}
ident := p.Identity()
return &ident, nil
}
func (p PasswordInfo) Identity() oidc.Identity {
return oidc.Identity{
ID: p.UserID,
}
}
type PasswordInfoRepo interface {
Get(tx repo.Transaction, id string) (PasswordInfo, error)
Update(repo.Transaction, PasswordInfo) error
Create(repo.Transaction, PasswordInfo) error
}
func (u *PasswordInfo) UnmarshalJSON(data []byte) error {
var dec struct {
UserID string `json:"userId"`
PasswordHash []byte `json:"passwordHash"`
PasswordPlaintext string `json:"passwordPlaintext"`
PasswordExpires time.Time `json:"passwordExpires"`
}
err := json.Unmarshal(data, &dec)
if err != nil {
return fmt.Errorf("invalid User entry: %v", err)
}
u.UserID = dec.UserID
if !dec.PasswordExpires.IsZero() {
u.PasswordExpires = dec.PasswordExpires
}
if len(dec.PasswordHash) != 0 {
if dec.PasswordPlaintext != "" {
return ErrorInvalidPassword
}
u.Password = Password(dec.PasswordHash)
return nil
}
if dec.PasswordPlaintext != "" {
u.Password, err = NewPasswordFromPlaintext(dec.PasswordPlaintext)
if err != nil {
return err
}
}
return nil
}
func LoadPasswordInfos(repo PasswordInfoRepo, pws []PasswordInfo) error {
for i, pw := range pws {
err := repo.Create(nil, pw)
if err != nil {
return fmt.Errorf("error loading PasswordInfo[%d]: %q", i, err)
}
}
return nil
}
func NewPasswordReset(userID string, password Password, issuer url.URL, clientID string, callback url.URL, expires time.Duration) PasswordReset {
claims := oidc.NewClaims(issuer.String(), userID, clientID, clock.Now(), clock.Now().Add(expires))
claims.Add(ClaimPasswordResetPassword, string(password))
claims.Add(ClaimPasswordResetCallback, callback.String())
return PasswordReset{claims}
}
type PasswordReset struct {
Claims jose.Claims
}
// 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
}
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 {
return assertStringClaim(e.Claims, "sub")
}
func (e PasswordReset) Password() Password {
pw := assertStringClaim(e.Claims, ClaimPasswordResetPassword)
return Password(pw)
}
func (e PasswordReset) Callback() *url.URL {
cb, ok, err := e.Claims.StringClaim(ClaimPasswordResetCallback)
if err != nil {
panic("PasswordReset: error getting string claim. This should be impossible.")
}
if !ok || cb == "" {
return nil
}
cbURL, err := url.Parse(cb)
if err != nil {
panic("PasswordReset: can't parse callback. This should be impossible.")
}
return cbURL
}