Files
core/iam/identity.go
2023-02-10 15:14:30 +01:00

715 lines
14 KiB
Go

package iam
import (
"encoding/json"
"fmt"
"os"
"regexp"
"sync"
"time"
"github.com/datarhei/core/v16/iam/jwks"
"github.com/datarhei/core/v16/io/fs"
"github.com/datarhei/core/v16/log"
"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"`
Superuser bool `json:"superuser"`
Auth UserAuth `json:"auth"`
}
type UserAuth struct {
API UserAuthAPI `json:"api"`
Services UserAuthServices `json:"services"`
}
type UserAuthAPI struct {
Userpass UserAuthPassword `json:"userpass"`
Auth0 UserAuthAPIAuth0 `json:"auth0"`
}
type UserAuthAPIAuth0 struct {
Enable bool `json:"enable"`
User string `json:"user"`
Tenant Auth0Tenant `json:"tenant"`
}
type UserAuthServices struct {
Basic UserAuthPassword `json:"basic"`
Token []string `json:"token"`
}
type UserAuthPassword struct {
Enable bool `json:"enable"`
Password string `json:"password"`
}
func (u *User) validate() error {
if len(u.Name) == 0 {
return fmt.Errorf("the name is required")
}
re := regexp.MustCompile(`[^A-Za-z0-9_-]`)
if re.MatchString(u.Name) {
return fmt.Errorf("the name can only the contain [A-Za-z0-9_-]")
}
if u.Auth.API.Userpass.Enable && len(u.Auth.API.Userpass.Password) == 0 {
return fmt.Errorf("a password for API login is required")
}
if u.Auth.API.Auth0.Enable && len(u.Auth.API.Auth0.User) == 0 {
return fmt.Errorf("a user for Auth0 login is required")
}
if u.Auth.Services.Basic.Enable && len(u.Auth.Services.Basic.Password) == 0 {
return fmt.Errorf("a password for service basic auth is required")
}
return nil
}
func (u *User) marshalIdentity() *identity {
i := &identity{
user: *u,
}
return i
}
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) VerifyAPIPassword(password string) (bool, error) {
i.lock.RLock()
defer i.lock.RUnlock()
if !i.isValid() {
return false, fmt.Errorf("invalid identity")
}
if !i.user.Auth.API.Userpass.Enable {
return false, fmt.Errorf("authentication method disabled")
}
return i.user.Auth.API.Userpass.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 !i.user.Auth.API.Auth0.Enable {
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) {
p := &jwtgo.Parser{}
token, _, err := p.ParseUnverified(jwt, jwtgo.MapClaims{})
if err != nil {
return false, err
}
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")
}
if token.Method.Alg() != "HS256" {
return false, fmt.Errorf("invalid hashing algorithm")
}
token, err = jwtgo.Parse(jwt, i.jwtKeyFunc)
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")
}
if !i.user.Auth.Services.Basic.Enable {
return false, fmt.Errorf("authentication method disabled")
}
return i.user.Auth.Services.Basic.Password == password, nil
}
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) isValid() bool {
return i.valid
}
func (i *identity) IsSuperuser() bool {
i.lock.RLock()
defer i.lock.RUnlock()
return i.user.Superuser
}
type IdentityVerifier interface {
Name() string
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)
IsSuperuser() bool
}
type IdentityManager interface {
Create(identity User) error
Remove(name string) error
Get(name string) (User, error)
GetVerifier(name string) (IdentityVerifier, error)
GetVerifierByAuth0(name string) (IdentityVerifier, error)
GetDefaultVerifier() (IdentityVerifier, error)
Rename(oldname, newname string) error
Update(name string, identity User) error
Validators() []string
CreateJWT(name string) (string, string, error)
Save() error
Close()
}
type identityManager struct {
root *identity
identities map[string]*identity
tenants map[string]*auth0Tenant
auth0UserIdentityMap map[string]string
fs fs.Filesystem
filePath string
logger log.Logger
jwtRealm string
jwtSecret []byte
lock sync.RWMutex
}
type IdentityConfig struct {
FS fs.Filesystem
Superuser User
JWTRealm string
JWTSecret string
Logger log.Logger
}
func NewIdentityManager(config IdentityConfig) (IdentityManager, error) {
im := &identityManager{
identities: map[string]*identity{},
tenants: map[string]*auth0Tenant{},
auth0UserIdentityMap: map[string]string{},
fs: config.FS,
filePath: "./users.json",
jwtRealm: config.JWTRealm,
jwtSecret: []byte(config.JWTSecret),
logger: config.Logger,
}
if im.logger == nil {
im.logger = log.New("")
}
err := im.load(im.filePath)
if err != nil {
return nil, err
}
config.Superuser.Superuser = true
identity, err := im.create(config.Superuser)
if err != nil {
return nil, err
}
im.root = identity
return im, nil
}
func (im *identityManager) Close() {
im.lock.Lock()
defer im.lock.Unlock()
im.fs = 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{}
return
}
func (im *identityManager) Create(u User) error {
if err := u.validate(); err != nil {
return err
}
im.lock.Lock()
defer im.lock.Unlock()
_, ok := im.identities[u.Name]
if ok {
return fmt.Errorf("identity already exists")
}
identity, err := im.create(u)
if err != nil {
return err
}
im.identities[identity.user.Name] = identity
return nil
}
func (im *identityManager) create(u User) (*identity, error) {
identity := u.marshalIdentity()
if identity.user.Auth.API.Auth0.Enable {
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 _, 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
}
}
identity.valid = true
return identity, nil
}
func (im *identityManager) Update(name string, identity User) error {
return nil
}
func (im *identityManager) Remove(name string) error {
im.lock.Lock()
defer im.lock.Unlock()
user, ok := im.identities[name]
if !ok {
return nil
}
delete(im.identities, name)
user.lock.Lock()
user.valid = false
user.lock.Unlock()
return nil
}
func (im *identityManager) getIdentity(name string) (*identity, error) {
var identity *identity = nil
if im.root.user.Name == name {
identity = im.root
} else {
identity, _ = im.identities[name]
}
if identity == nil {
return nil, fmt.Errorf("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{}, fmt.Errorf("not found")
}
return identity.user, nil
}
func (im *identityManager) GetVerifier(name string) (IdentityVerifier, error) {
im.lock.RLock()
defer im.lock.RUnlock()
return im.getIdentity(name)
}
func (im *identityManager) GetVerifierByAuth0(name string) (IdentityVerifier, 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() (IdentityVerifier, error) {
return im.root, nil
}
func (im *identityManager) Rename(oldname, newname string) error {
im.lock.Lock()
defer im.lock.Unlock()
identity, ok := im.identities[oldname]
if !ok {
return nil
}
if _, ok := im.identities[newname]; ok {
return fmt.Errorf("the new name already exists")
}
delete(im.identities, oldname)
identity.user.Name = newname
im.identities[newname] = identity
return nil
}
func (im *identityManager) load(filePath string) error {
if im.fs == nil {
return fmt.Errorf("no filesystem provided")
}
if _, err := im.fs.Stat(filePath); os.IsNotExist(err) {
return nil
}
data, err := im.fs.ReadFile(filePath)
if err != nil {
return err
}
users := []User{}
err = json.Unmarshal(data, &users)
if err != nil {
return err
}
for _, u := range users {
err = im.Create(u)
if err != nil {
return err
}
}
return nil
}
func (im *identityManager) Save() error {
return im.save(im.filePath)
}
func (im *identityManager) save(filePath string) error {
if im.fs == nil {
return fmt.Errorf("no filesystem provided")
}
if filePath == "" {
return fmt.Errorf("invalid file path, file path cannot be empty")
}
im.lock.RLock()
defer im.lock.RUnlock()
users := []User{}
for _, u := range im.identities {
users = append(users, u.user)
}
jsondata, err := json.MarshalIndent(users, "", " ")
if err != nil {
return err
}
_, _, err = im.fs.WriteFileSafe(filePath, jsondata)
return err
}
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) {
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": 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": 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
}
type Auth0Tenant struct {
Domain string
Audience string
ClientID string
}
func (t *Auth0Tenant) key() string {
return t.Domain + t.Audience
}
type auth0Tenant struct {
domain string
issuer string
audience string
clientIDs []string
certs jwks.JWKS
}
func newAuth0Tenant(tenant Auth0Tenant) (*auth0Tenant, error) {
t := &auth0Tenant{
domain: tenant.Domain,
issuer: "https://" + tenant.Domain + "/",
audience: tenant.Audience,
clientIDs: []string{tenant.ClientID},
certs: nil,
}
url := t.issuer + "/.well-known/jwks.json"
certs, err := jwks.NewFromURL(url, jwks.Config{})
if err != nil {
return nil, err
}
t.certs = certs
return t, nil
}
func (a *auth0Tenant) Cancel() {
a.certs.Cancel()
}