mirror of
https://github.com/datarhei/core.git
synced 2025-10-05 16:07:07 +08:00
715 lines
14 KiB
Go
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()
|
|
}
|