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 }