dex/user/user.go

287 lines
7.4 KiB
Go
Raw Normal View History

2015-08-18 05:57:27 +05:30
package user
import (
"encoding/json"
"errors"
"fmt"
2016-05-17 01:41:33 +05:30
"math"
2015-08-18 05:57:27 +05:30
"time"
"net/mail"
"net/url"
2015-08-18 05:57:27 +05:30
"github.com/jonboulle/clockwork"
"github.com/pborman/uuid"
2015-08-18 05:57:27 +05:30
"github.com/coreos/dex/repo"
"github.com/coreos/go-oidc/jose"
"github.com/coreos/go-oidc/key"
"github.com/coreos/go-oidc/oidc"
2015-08-18 05:57:27 +05:30
)
const (
MaxEmailLength = 200
// ClaimPasswordResetPassword represents the hash of the password to be
// reset; in other words, the old password
ClaimPasswordResetPassword = "http://coreos.com/password/old-hash"
// ClaimEmailVerificationEmail represents the email to be verified. Note
// that we are intentionally not using the "email" claim for this purpose.
ClaimEmailVerificationEmail = "http://coreos.com/email/verificationEmail"
// ClaimPasswordResetCallback represents where a user should be sent after
// resetting their password.
ClaimPasswordResetCallback = "http://coreos.com/password/reset-callback"
// Claim representing where a user should be sent after verifying their email address.
ClaimEmailVerificationCallback = "http://coreos.com/email/verification-callback"
// Claim representing where a user should be sent after responding to an invitation
ClaimInvitationCallback = "http://coreos.com/invitation/callback"
2015-08-18 05:57:27 +05:30
)
var (
clock = clockwork.NewRealClock()
)
func assertStringClaim(claims jose.Claims, k string) string {
s, ok, err := claims.StringClaim(k)
if !ok || err != nil {
panic(fmt.Sprintf("claims were not validated correctly, missing or wrong claim: %v", k))
}
return s
}
func assertURLClaim(claims jose.Claims, k string) *url.URL {
ustring := assertStringClaim(claims, k)
ret, err := url.Parse(ustring)
if err != nil {
panic(fmt.Sprintf("url claim was not validated correctly: %v", k))
}
return ret
}
2015-08-18 05:57:27 +05:30
type UserIDGenerator func() (string, error)
func DefaultUserIDGenerator() (string, error) {
return uuid.New(), nil
}
type User struct {
// ID is the machine-generated, stable, unique identifier for this User.
ID string
// DisplayName is human readable name meant for display purposes.
// DisplayName is not neccesarily unique with a UserRepo.
DisplayName string
Email string
EmailVerified bool
Admin bool
Disabled bool
2015-08-18 05:57:27 +05:30
CreatedAt time.Time
}
type UserFilter struct {
// TODO(bobbyrullo): actually put stuff in here.
}
// AddToClaims adds basic information about the user to the given Claims.
// http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
func (u *User) AddToClaims(claims jose.Claims) {
claims.Add("name", u.DisplayName)
if u.Email != "" {
claims.Add("email", u.Email)
if u.EmailVerified {
claims.Add("email_verified", true)
}
}
}
// UserRepo implementations maintain a persistent set of users.
// The following invariants must be maintained:
// * Users must have a unique Email and ID
// * Emails are case insensitive.
2015-08-18 05:57:27 +05:30
// * No other Users may have the same RemoteIdentity as one of the
// users. (This constraint may be relaxed in the future)
type UserRepo interface {
Get(tx repo.Transaction, id string) (User, error)
// List returns a list of users meeting the given conditions.
// A nextPageToken is returned when there are further results to be had,
// with the expectation that it will be passed into a subsequent List
// call. When nextPageToken is non-empty filter and maxResults are ignored.
List(tx repo.Transaction, filter UserFilter, maxResults int, nextPageToken string) ([]User, string, error)
Create(repo.Transaction, User) error
GetByEmail(tx repo.Transaction, email string) (User, error)
Disable(tx repo.Transaction, id string, disabled bool) error
2015-08-18 05:57:27 +05:30
Update(repo.Transaction, User) error
GetByRemoteIdentity(repo.Transaction, RemoteIdentity) (User, error)
AddRemoteIdentity(tx repo.Transaction, userID string, remoteID RemoteIdentity) error
RemoveRemoteIdentity(tx repo.Transaction, userID string, remoteID RemoteIdentity) error
GetRemoteIdentities(tx repo.Transaction, userID string) ([]RemoteIdentity, error)
GetAdminCount(repo.Transaction) (int, error)
}
var (
ErrorDuplicateID = errors.New("ID not available")
ErrorDuplicateEmail = errors.New("email not available")
ErrorDuplicateRemoteIdentity = errors.New("remote identity already in use for another user")
ErrorInvalidEmail = errors.New("invalid Email")
ErrorInvalidID = errors.New("invalid ID")
ErrorNotFound = errors.New("user not found in repository")
)
// RemoteIdentity represents a User's identity at an IDP.
type RemoteIdentity struct {
// IDPCID is the identifier of the IDP which hosts this identity.
ConnectorID string
// ID is the identifier of this User at the IDP.
ID string
}
func ValidEmail(email string) bool {
address, err := mail.ParseAddress(email)
if err != nil {
return false
}
// Has mail.ParseAddress parsed the entire string and only found a single
// address without a name?
return address.Address == email
2015-08-18 05:57:27 +05:30
}
func ValidPassword(plaintext string) bool {
return len(plaintext) > 5
}
type UserWithRemoteIdentities struct {
User User `json:"user"`
RemoteIdentities []RemoteIdentity `json:"remoteIdentities"`
}
func (u *User) UnmarshalJSON(data []byte) error {
var dec struct {
ID string `json:"id"`
DisplayName string `json:"displayName"`
Email string `json:"email"`
EmailVerified bool `json:"emailVerified"`
}
err := json.Unmarshal(data, &dec)
if err != nil {
return fmt.Errorf("invalid User entry: %v", err)
}
u.ID = dec.ID
u.DisplayName = dec.DisplayName
u.Email = dec.Email
u.EmailVerified = dec.EmailVerified
return nil
}
func (u *RemoteIdentity) UnmarshalJSON(data []byte) error {
var dec struct {
ConnectorID string `json:"connectorID"`
ID string `json:"id"`
}
err := json.Unmarshal(data, &dec)
if err != nil {
return fmt.Errorf("invalid RemoteIdentity entry: %v", err)
}
u.ID = dec.ID
u.ConnectorID = dec.ConnectorID
return nil
}
type TokenClaims struct {
Claims jose.Claims
}
// Returns TokenClaims if and only if
// - the given token string is an appropriately formatted JWT
// - the JWT contains nonempty "aud" and "sub" claims
// - the JWT can be verified for the client associated with the "aud" claim
// using the given keys
func parseAndVerifyTokenClaims(token string, issuer url.URL, keys []key.PublicKey) (TokenClaims, error) {
jwt, err := jose.ParseJWT(token)
if err != nil {
return TokenClaims{}, err
}
claims, err := jwt.Claims()
if err != nil {
return TokenClaims{}, err
}
clientID, ok, err := claims.StringClaim("aud")
if err != nil {
return TokenClaims{}, err
}
if !ok || clientID == "" {
return TokenClaims{}, errors.New("no aud(client ID) claim")
}
sub, ok, err := claims.StringClaim("sub")
if err != nil {
return TokenClaims{}, err
}
if !ok || sub == "" {
return TokenClaims{}, errors.New("no sub claim")
}
noop := func() error { return nil }
keysFunc := func() []key.PublicKey {
return keys
}
verifier := oidc.NewJWTVerifier(issuer.String(), clientID, noop, keysFunc)
if err := verifier.Verify(jwt); err != nil {
return TokenClaims{}, err
}
2016-05-17 01:41:33 +05:30
timeClaimsToInt(claims)
return TokenClaims{claims}, nil
}
2016-05-17 01:41:33 +05:30
// timeClaimsToInt converts float64 time claims to ints.
// This is unfortunately neccessary for interop as some clients incorrectly fail
// to marshal floats as times.
func timeClaimsToInt(claims jose.Claims) {
for _, k := range []string{"exp", "iat"} {
v, ok := claims[k]
if !ok {
continue
}
fVal, ok := v.(float64)
if !ok {
continue
}
// round
claims[k] = int64(fVal + math.Copysign(0.5, fVal))
}
}