package identity import ( "fmt" "strings" "sync" "time" enctoken "github.com/datarhei/core/v16/encoding/token" "github.com/datarhei/core/v16/log" "github.com/datarhei/core/v16/slices" "github.com/google/uuid" jwtgo "github.com/golang-jwt/jwt/v4" ) // Auth0 // there needs to be a mapping from the Auth.User to Name // the same Auth0.User can't have multiple identities // the whole jwks will be part of this package type User struct { Name string `json:"name"` Alias string `json:"alias"` Superuser bool `json:"superuser"` Auth UserAuth `json:"auth"` } type UserAuth struct { API UserAuthAPI `json:"api"` Services UserAuthServices `json:"services"` } type UserAuthAPI struct { Password string `json:"password"` Auth0 UserAuthAPIAuth0 `json:"auth0"` } type UserAuthAPIAuth0 struct { User string `json:"user"` Tenant Auth0Tenant `json:"tenant"` } type UserAuthServices struct { Basic []string `json:"basic"` // Passwords for BasicAuth Token []string `json:"token"` // Tokens/Streamkey for RTMP and SRT Session []string `json:"session"` // Secrets for session JWT } func (u *User) Validate() error { if len(u.Name) == 0 { return fmt.Errorf("a name is required") } if strings.HasPrefix(u.Name, "$") { return fmt.Errorf("name is not allowed to start with $") } if len(u.Alias) != 0 { if strings.HasPrefix(u.Alias, "$") { return fmt.Errorf("alias is not allowed to start with $") } } if len(u.Auth.API.Auth0.User) != 0 { t, err := newAuth0Tenant(u.Auth.API.Auth0.Tenant) if err != nil { return fmt.Errorf("auth0: %w", err) } t.Cancel() } return nil } func (u *User) marshalIdentity() *identity { i := &identity{ user: *u, } return i } func (u *User) clone() User { user := *u user.Auth.Services.Basic = slices.Copy(u.Auth.Services.Basic) user.Auth.Services.Token = slices.Copy(u.Auth.Services.Token) user.Auth.Services.Session = slices.Copy(u.Auth.Services.Session) return user } type Verifier interface { Name() string // Name returns the name of the identity. Alias() string // Alias returns the alias of the identity, or an empty string if no alias has been set. VerifyJWT(jwt string) (bool, error) VerifyAPIPassword(password string) (bool, error) VerifyAPIAuth0(jwt string) (bool, error) VerifyServiceBasicAuth(password string) (bool, error) VerifyServiceToken(token string) (bool, error) VerifyServiceSession(jwt string) (bool, interface{}, error) GetServiceBasicAuth() string GetServiceToken() string GetServiceSession(interface{}, time.Duration) string IsSuperuser() bool } type identity struct { user User tenant *auth0Tenant jwtRealm string jwtKeyFunc func(*jwtgo.Token) (interface{}, error) valid bool lock sync.RWMutex } func (i *identity) Name() string { return i.user.Name } func (i *identity) Alias() string { return i.user.Alias } func (i *identity) VerifyAPIPassword(password string) (bool, error) { i.lock.RLock() defer i.lock.RUnlock() if !i.isValid() { return false, fmt.Errorf("invalid identity") } if len(i.user.Auth.API.Password) == 0 { return false, fmt.Errorf("authentication method disabled") } return i.user.Auth.API.Password == password, nil } func (i *identity) VerifyAPIAuth0(jwt string) (bool, error) { i.lock.RLock() defer i.lock.RUnlock() if !i.isValid() { return false, fmt.Errorf("invalid identity") } if len(i.user.Auth.API.Auth0.User) == 0 { return false, fmt.Errorf("authentication method disabled") } p := &jwtgo.Parser{} token, _, err := p.ParseUnverified(jwt, jwtgo.MapClaims{}) if err != nil { return false, err } var subject string if claims, ok := token.Claims.(jwtgo.MapClaims); ok { if sub, ok := claims["sub"]; ok { subject = sub.(string) } } if subject != i.user.Auth.API.Auth0.User { return false, fmt.Errorf("wrong subject") } var issuer string if claims, ok := token.Claims.(jwtgo.MapClaims); ok { if iss, ok := claims["iss"]; ok { issuer = iss.(string) } } if issuer != i.tenant.issuer { return false, fmt.Errorf("wrong issuer") } token, err = jwtgo.Parse(jwt, i.auth0KeyFunc) if err != nil { return false, err } if !token.Valid { return false, fmt.Errorf("invalid token") } return true, nil } func (i *identity) auth0KeyFunc(token *jwtgo.Token) (interface{}, error) { // Verify 'aud' claim checkAud := token.Claims.(jwtgo.MapClaims).VerifyAudience(i.tenant.audience, false) if !checkAud { return nil, fmt.Errorf("invalid audience") } // Verify 'iss' claim checkIss := token.Claims.(jwtgo.MapClaims).VerifyIssuer(i.tenant.issuer, false) if !checkIss { return nil, fmt.Errorf("invalid issuer") } // Verify 'sub' claim if _, ok := token.Claims.(jwtgo.MapClaims)["sub"]; !ok { return nil, fmt.Errorf("sub claim is required") } // find the key if _, ok := token.Header["kid"]; !ok { return nil, fmt.Errorf("kid not found") } kid := token.Header["kid"].(string) key, err := i.tenant.certs.Key(kid) if err != nil { return nil, fmt.Errorf("no cert for kid found: %w", err) } // find algorithm if _, ok := token.Header["alg"]; !ok { return nil, fmt.Errorf("kid not found") } alg := token.Header["alg"].(string) if key.Alg() != alg { return nil, fmt.Errorf("signing method doesn't match") } // get the public key publicKey, err := key.PublicKey() if err != nil { return nil, fmt.Errorf("invalid public key: %w", err) } return publicKey, nil } func (i *identity) VerifyJWT(jwt string) (bool, error) { i.lock.RLock() defer i.lock.RUnlock() if !i.isValid() { return false, fmt.Errorf("invalid identity") } p := &jwtgo.Parser{} token, _, err := p.ParseUnverified(jwt, jwtgo.MapClaims{}) if err != nil { return false, err } var subject string if claims, ok := token.Claims.(jwtgo.MapClaims); ok { if sub, ok := claims["sub"]; ok { subject = sub.(string) } } if subject != i.user.Name { return false, fmt.Errorf("wrong subject") } var issuer string if claims, ok := token.Claims.(jwtgo.MapClaims); ok { if sub, ok := claims["iss"]; ok { issuer = sub.(string) } } if issuer != i.jwtRealm { return false, fmt.Errorf("wrong issuer") } token, err = jwtgo.Parse(jwt, i.jwtKeyFunc, jwtgo.WithValidMethods([]string{"HS256"})) if err != nil { return false, err } if !token.Valid { return false, fmt.Errorf("invalid token") } return true, nil } func (i *identity) VerifyServiceBasicAuth(password string) (bool, error) { i.lock.RLock() defer i.lock.RUnlock() if !i.isValid() { return false, fmt.Errorf("invalid identity") } for _, pw := range i.user.Auth.Services.Basic { if len(pw) == 0 { continue } if pw == password { return true, nil } } return false, nil } func (i *identity) GetServiceBasicAuth() string { i.lock.RLock() defer i.lock.RUnlock() if !i.isValid() { return "" } for _, password := range i.user.Auth.Services.Basic { if len(password) == 0 { continue } return password } return "" } func (i *identity) VerifyServiceToken(token string) (bool, error) { i.lock.RLock() defer i.lock.RUnlock() if !i.isValid() { return false, fmt.Errorf("invalid identity") } for _, t := range i.user.Auth.Services.Token { if t == token { return true, nil } } return false, nil } func (i *identity) GetServiceToken() string { i.lock.RLock() defer i.lock.RUnlock() if !i.isValid() { return "" } for _, token := range i.user.Auth.Services.Token { if len(token) == 0 { continue } return enctoken.Marshal(i.Name(), token) } return "" } func (i *identity) VerifyServiceSession(jwt string) (bool, interface{}, error) { i.lock.RLock() defer i.lock.RUnlock() if !i.isValid() { return false, nil, fmt.Errorf("invalid identity") } if len(i.user.Auth.Services.Session) == 0 { return false, nil, nil } p := &jwtgo.Parser{} token, _, err := p.ParseUnverified(jwt, jwtgo.MapClaims{}) if err != nil { return false, nil, err } claims, ok := token.Claims.(jwtgo.MapClaims) if !ok { return false, nil, fmt.Errorf("invalid claims") } var subject string if sub, ok := claims["sub"]; ok { subject = sub.(string) } if subject != i.user.Name && subject != i.user.Alias { return false, nil, fmt.Errorf("wrong subject") } var issuer string if sub, ok := claims["iss"]; ok { issuer = sub.(string) } if issuer != i.jwtRealm { return false, nil, fmt.Errorf("wrong issuer") } for _, secret := range i.user.Auth.Services.Session { fn := func(*jwtgo.Token) (interface{}, error) { return []byte(secret), nil } token, err = jwtgo.Parse(jwt, fn, jwtgo.WithValidMethods([]string{"HS256"})) if err == nil { break } } if err != nil { return false, nil, fmt.Errorf("parse: %w", err) } if !token.Valid { return false, nil, fmt.Errorf("invalid token") } return true, claims["data"], nil } func (i *identity) GetServiceSession(data interface{}, ttl time.Duration) string { i.lock.RLock() defer i.lock.RUnlock() if !i.isValid() { return "" } if len(i.user.Auth.Services.Session) == 0 { return "" } now := time.Now() accessExpires := now.Add(ttl) // Create access token accessToken := jwtgo.NewWithClaims(jwtgo.SigningMethodHS256, jwtgo.MapClaims{ "iss": i.jwtRealm, "sub": i.user.Name, "iat": now.Unix(), "exp": accessExpires.Unix(), "data": data, }) // Generate encoded access token at, err := accessToken.SignedString([]byte(i.user.Auth.Services.Session[0])) if err != nil { return "" } return at } func (i *identity) isValid() bool { return i.valid } func (i *identity) IsSuperuser() bool { i.lock.RLock() defer i.lock.RUnlock() return i.user.Superuser } type Manager interface { Create(identity User) error Update(name string, identity User) error Delete(name string) error Get(name string) (User, error) GetVerifier(name string) (Verifier, error) GetVerifierFromAuth0(name string) (Verifier, error) GetDefaultVerifier() (Verifier, error) Reload() error // Reload users from adapter Save() error // Save users to adapter List() []User // List all users Validators() []string CreateJWT(name string) (string, string, error) Close() } type identityManager struct { root *identity identities map[string]*identity tenants map[string]*auth0Tenant auth0UserIdentityMap map[string]string adapter Adapter autosave bool logger log.Logger jwtRealm string jwtSecret []byte lock sync.RWMutex } type Config struct { Adapter Adapter Superuser User JWTRealm string JWTSecret string Logger log.Logger } func New(config Config) (Manager, error) { im := &identityManager{ identities: map[string]*identity{}, tenants: map[string]*auth0Tenant{}, auth0UserIdentityMap: map[string]string{}, adapter: config.Adapter, jwtRealm: config.JWTRealm, jwtSecret: []byte(config.JWTSecret), logger: config.Logger, } if im.logger == nil { im.logger = log.New("") } if im.adapter == nil { return nil, fmt.Errorf("no adapter provided") } config.Superuser.Superuser = true identity, err := im.create(config.Superuser) if err != nil { return nil, err } im.root = identity err = im.Reload() if err != nil { return nil, err } im.Save() return im, nil } func (im *identityManager) Close() { im.lock.Lock() defer im.lock.Unlock() im.adapter = nil im.auth0UserIdentityMap = map[string]string{} im.identities = map[string]*identity{} im.root = nil for _, t := range im.tenants { t.Cancel() } im.tenants = map[string]*auth0Tenant{} } func (im *identityManager) Reload() error { users, err := im.adapter.LoadIdentities() if err != nil { return fmt.Errorf("load users from adapter: %w", err) } im.lock.Lock() defer im.lock.Unlock() im.autosave = false defer func() { im.autosave = true }() names := []string{} for name := range im.identities { im.delete(name) } for _, name := range names { im.delete(name) } for _, u := range users { if ok, _ := im.isNameAvailable(u.Name, u.Alias); !ok { continue } if err := u.Validate(); err != nil { return fmt.Errorf("invalid user from adapter: %s, %w", u.Name, err) } identity, err := im.create(u) if err != nil { continue } im.identities[identity.user.Name] = identity if len(identity.user.Alias) != 0 { im.identities[identity.user.Alias] = identity } } return nil } func (im *identityManager) isNameAvailable(name, alias string) (bool, error) { if im.root == nil { return true, nil } if name == im.root.user.Name { return false, fmt.Errorf("name already exists") } if name == im.root.user.Alias { return false, fmt.Errorf("name already exists") } if _, ok := im.identities[name]; ok { return false, fmt.Errorf("name already exists") } if len(alias) != 0 { if alias == im.root.user.Name { return false, fmt.Errorf("alias already exists") } if alias == im.root.user.Alias { return false, fmt.Errorf("alias already exists") } if _, ok := im.identities[alias]; ok { return false, fmt.Errorf("alias already exists") } } return true, nil } func (im *identityManager) Create(u User) error { if err := u.Validate(); err != nil { return err } im.lock.Lock() defer im.lock.Unlock() if ok, err := im.isNameAvailable(u.Name, u.Alias); !ok { return err } identity, err := im.create(u) if err != nil { return err } im.identities[identity.user.Name] = identity if len(identity.user.Alias) != 0 { im.identities[identity.user.Alias] = identity } if im.autosave { im.save() } return nil } func (im *identityManager) create(u User) (*identity, error) { u = u.clone() identity := u.marshalIdentity() if len(identity.user.Auth.API.Auth0.User) != 0 { if _, ok := im.auth0UserIdentityMap[identity.user.Auth.API.Auth0.User]; ok { return nil, fmt.Errorf("the Auth0 user has already an identity") } auth0Key := identity.user.Auth.API.Auth0.Tenant.key() if tenant, ok := im.tenants[auth0Key]; !ok { tenant, err := newAuth0Tenant(identity.user.Auth.API.Auth0.Tenant) if err != nil { return nil, err } im.tenants[auth0Key] = tenant identity.tenant = tenant } else { tenant.AddClientID(identity.user.Auth.API.Auth0.Tenant.ClientID) identity.tenant = tenant } im.auth0UserIdentityMap[identity.user.Auth.API.Auth0.User] = u.Name } identity.valid = true im.logger.Debug().WithField("name", identity.Name()).Log("Identity created") return identity, nil } func (im *identityManager) Update(name string, u User) error { if err := u.Validate(); err != nil { return err } im.lock.Lock() defer im.lock.Unlock() if im.root.user.Name == name || im.root.user.Alias == name { return fmt.Errorf("this identity cannot be updated") } oldidentity, ok := im.identities[name] if !ok { return fmt.Errorf("identity not found") } delete(im.identities, oldidentity.user.Name) delete(im.identities, oldidentity.user.Alias) ok, err := im.isNameAvailable(u.Name, u.Alias) im.identities[oldidentity.user.Name] = oldidentity if len(oldidentity.user.Alias) != 0 { im.identities[oldidentity.user.Alias] = oldidentity } if !ok { return err } err = im.delete(name) if err != nil { return err } identity, err := im.create(u) if err != nil { // restore old identity im.create(oldidentity.user) im.identities[oldidentity.user.Name] = oldidentity if len(oldidentity.user.Alias) != 0 { im.identities[oldidentity.user.Alias] = oldidentity } return err } im.identities[identity.user.Name] = identity if len(identity.user.Alias) != 0 { im.identities[identity.user.Alias] = identity } im.logger.Debug().WithFields(log.Fields{ "oldname": name, "newname": identity.Name(), }).Log("Identity updated") if im.autosave { im.save() } return nil } func (im *identityManager) Delete(name string) error { im.lock.Lock() defer im.lock.Unlock() err := im.delete(name) if err != nil { return err } return nil } func (im *identityManager) delete(name string) error { if im.root.user.Name == name || im.root.user.Alias == name { return fmt.Errorf("this identity can't be removed") } identity, ok := im.identities[name] if !ok { return fmt.Errorf("identity not found") } delete(im.identities, identity.user.Name) delete(im.identities, identity.user.Alias) identity.lock.Lock() identity.valid = false identity.lock.Unlock() if len(identity.user.Auth.API.Auth0.User) == 0 { if im.autosave { im.save() } return nil } delete(im.auth0UserIdentityMap, identity.user.Auth.API.Auth0.User) // find out if the tenant is still used somewhere else found := false for _, i := range im.identities { if i.tenant == identity.tenant { found = true break } } if !found { identity.tenant.Cancel() delete(im.tenants, identity.user.Auth.API.Auth0.Tenant.key()) if im.autosave { im.save() } return nil } // find out if the tenant's clientid is still used somewhere else found = false for _, i := range im.identities { if len(i.user.Auth.API.Auth0.User) == 0 { continue } if i.user.Auth.API.Auth0.Tenant.ClientID == identity.user.Auth.API.Auth0.Tenant.ClientID { found = true break } } if !found { identity.tenant.RemoveClientID(identity.user.Auth.API.Auth0.Tenant.ClientID) } if im.autosave { if err := im.save(); err != nil { return err } } return nil } func (im *identityManager) getIdentity(name string) (*identity, error) { var identity *identity = nil if im.root.user.Name == name || im.root.user.Alias == name { identity = im.root } else { identity = im.identities[name] } if identity == nil { return nil, fmt.Errorf("identity not found") } identity.jwtRealm = im.jwtRealm identity.jwtKeyFunc = func(*jwtgo.Token) (interface{}, error) { return im.jwtSecret, nil } return identity, nil } func (im *identityManager) Get(name string) (User, error) { im.lock.RLock() defer im.lock.RUnlock() identity, err := im.getIdentity(name) if err != nil { return User{}, err } user := identity.user.clone() return user, nil } func (im *identityManager) GetVerifier(name string) (Verifier, error) { im.lock.RLock() defer im.lock.RUnlock() return im.getIdentity(name) } func (im *identityManager) GetVerifierFromAuth0(name string) (Verifier, error) { im.lock.RLock() defer im.lock.RUnlock() name, ok := im.auth0UserIdentityMap[name] if !ok { return nil, fmt.Errorf("not found") } return im.getIdentity(name) } func (im *identityManager) GetDefaultVerifier() (Verifier, error) { return im.root, nil } func (im *identityManager) List() []User { im.lock.RLock() defer im.lock.RUnlock() users := []User{} for _, identity := range im.identities { users = append(users, identity.user.clone()) } return users } func (im *identityManager) Save() error { im.lock.RLock() defer im.lock.RUnlock() return im.save() } func (im *identityManager) save() error { users := []User{} for _, u := range im.identities { users = append(users, u.user) } return im.adapter.SaveIdentities(users) } func (im *identityManager) Autosave(auto bool) { im.lock.Lock() defer im.lock.Unlock() im.autosave = auto } func (im *identityManager) Validators() []string { validators := []string{"localjwt"} im.lock.RLock() defer im.lock.RUnlock() for _, t := range im.tenants { for _, clientid := range t.clientIDs { validators = append(validators, fmt.Sprintf("auth0 domain=%s audience=%s clientid=%s", t.domain, t.audience, clientid)) } } return validators } func (im *identityManager) CreateJWT(name string) (string, string, error) { im.lock.RLock() defer im.lock.RUnlock() identity, err := im.getIdentity(name) if err != nil { return "", "", err } now := time.Now() accessExpires := now.Add(time.Minute * 10) refreshExpires := now.Add(time.Hour * 24) // Create access token accessToken := jwtgo.NewWithClaims(jwtgo.SigningMethodHS256, jwtgo.MapClaims{ "iss": im.jwtRealm, "sub": identity.Name(), "usefor": "access", "iat": now.Unix(), "exp": accessExpires.Unix(), "exi": uint64(accessExpires.Sub(now).Seconds()), "jti": uuid.New().String(), }) // Generate encoded access token at, err := accessToken.SignedString(im.jwtSecret) if err != nil { return "", "", err } // Create refresh token refreshToken := jwtgo.NewWithClaims(jwtgo.SigningMethodHS256, jwtgo.MapClaims{ "iss": im.jwtRealm, "sub": identity.Name(), "usefor": "refresh", "iat": now.Unix(), "exp": refreshExpires.Unix(), "exi": uint64(refreshExpires.Sub(now).Seconds()), "jti": uuid.New().String(), }) // Generate encoded refresh token rt, err := refreshToken.SignedString(im.jwtSecret) if err != nil { return "", "", err } return at, rt, nil }