// Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 package auth import ( "context" "crypto/rand" "encoding/base64" "strings" "time" "github.com/absmach/magistrala" "github.com/absmach/magistrala/pkg/errors" repoerr "github.com/absmach/magistrala/pkg/errors/repository" svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/absmach/magistrala/pkg/policies" "github.com/google/uuid" ) const ( recoveryDuration = 5 * time.Minute randStr = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&&*|+-=" patPrefix = "pat" patSecretSeparator = "_" ) var ( // ErrExpiry indicates that the token is expired. ErrExpiry = errors.New("token is expired") errIssueUser = errors.New("failed to issue new login key") errIssueTmp = errors.New("failed to issue new temporary key") errRevoke = errors.New("failed to remove key") errRetrieve = errors.New("failed to retrieve key data") errIdentify = errors.New("failed to validate token") errPlatform = errors.New("invalid platform id") errRoleAuth = errors.New("failed to authorize user role") errSaveRefreshKey = errors.NewServiceError("failed to save refresh key") errRevokeRefreshKey = errors.NewServiceError("failed to revoke refresh key") errListRefreshKeys = errors.NewServiceError("failed to list refresh keys") errMalformedPAT = errors.New("malformed personal access token") errFailedToParseUUID = errors.New("failed to parse string to UUID") errInvalidLenFor2UUIDs = errors.New("invalid input length for 2 UUID, excepted 32 byte") errRevokedPAT = errors.NewServiceError("revoked pat") errCreatePAT = errors.NewServiceError("failed to create PAT") errUpdatePAT = errors.NewServiceError("failed to update PAT") errRetrievePAT = errors.NewServiceError("failed to retrieve PAT") errDeletePAT = errors.NewServiceError("failed to delete PAT") errInvalidScope = errors.New("invalid scope") ) // Authz represents a authorization service. It exposes // functionalities through `auth` to perform authorization. type Authz interface { // Authorize checks authorization of the given `subject`. Basically, // Authorize verifies that Is `subject` allowed to `relation` on // `object`. Authorize returns a non-nil error if the subject has // no relation on the object (which simply means the operation is // denied). Authorize(ctx context.Context, pr policies.Policy, patAuthz *PATAuthz) error } // Authn specifies an API that must be fulfilled by the domain service // implementation, and all of its decorators (e.g. logging & metrics). // Token is a string value of the actual Key and is used to authenticate // an Auth service request. type Authn interface { // Issue issues a new Key, returning its token value alongside. Issue(ctx context.Context, token string, key Key) (Token, error) // RevokeToken revokes the refresh token by its ID. RevokeToken(ctx context.Context, userID, tokenID string) error // Revoke removes the Key with the provided id that is // issued by the user identified by the provided key. Revoke(ctx context.Context, token, id string) error // RetrieveKey retrieves data for the Key identified by the provided // ID, that is issued by the user identified by the provided key. RetrieveKey(ctx context.Context, token, id string) (Key, error) // Identify validates token token. If token is valid, content // is returned. If token is invalid, or invocation failed for some // other reason, non-nil error value is returned in response. Identify(ctx context.Context, token string) (Key, error) // RetrieveJWKS retrieves public keys to validate issued tokens. RetrieveJWKS() []PublicKeyInfo // ListUserRefreshTokens lists all active refresh token sessions for a user. ListUserRefreshTokens(ctx context.Context, userID string) ([]TokenInfo, error) } // Service specifies an API that must be fulfilled by the domain service // implementation, and all of its decorators (e.g. logging & metrics). // Token is a string value of the actual Key and is used to authenticate // an Auth service request. type Service interface { Authn Authz PATS } var _ Service = (*service)(nil) type service struct { keys KeyRepository pats PATSRepository cache Cache tokensCache UserActiveTokensCache hasher Hasher idProvider magistrala.IDProvider evaluator policies.Evaluator policysvc policies.Service tokenizer Tokenizer loginDuration time.Duration refreshDuration time.Duration invitationDuration time.Duration } // New instantiates the auth service implementation. func New(keys KeyRepository, pats PATSRepository, cache Cache, tokensCache UserActiveTokensCache, hasher Hasher, idp magistrala.IDProvider, tokenizer Tokenizer, policyEvaluator policies.Evaluator, policyService policies.Service, loginDuration, refreshDuration, invitationDuration time.Duration) Service { return &service{ tokenizer: tokenizer, keys: keys, pats: pats, cache: cache, tokensCache: tokensCache, hasher: hasher, idProvider: idp, evaluator: policyEvaluator, policysvc: policyService, loginDuration: loginDuration, refreshDuration: refreshDuration, invitationDuration: invitationDuration, } } func (svc service) Issue(ctx context.Context, token string, key Key) (Token, error) { key.IssuedAt = time.Now().UTC() switch key.Type { case APIKey: return svc.userKey(ctx, token, key) case RefreshKey: return svc.refreshKey(ctx, token, key) case RecoveryKey: return svc.tmpKey(ctx, recoveryDuration, key) case InvitationKey: return svc.invitationKey(ctx, key) default: return svc.accessKey(ctx, key) } } func (svc service) RevokeToken(ctx context.Context, userID, tokenID string) error { if err := svc.tokensCache.RemoveActive(ctx, userID, tokenID); err != nil { return errors.Wrap(errRevokeRefreshKey, err) } return nil } func (svc service) Revoke(ctx context.Context, token, id string) error { issuerID, _, err := svc.authenticate(ctx, token) if err != nil { return errors.Wrap(errRevoke, err) } if err := svc.keys.Remove(ctx, issuerID, id); err != nil { return errors.Wrap(errRevoke, err) } return nil } func (svc service) RetrieveKey(ctx context.Context, token, id string) (Key, error) { issuerID, _, err := svc.authenticate(ctx, token) if err != nil { return Key{}, errors.Wrap(errRetrieve, err) } key, err := svc.keys.Retrieve(ctx, issuerID, id) if err != nil { return Key{}, errors.Wrap(svcerr.ErrViewEntity, err) } return key, nil } func (svc service) Identify(ctx context.Context, token string) (Key, error) { key, err := svc.tokenizer.Parse(ctx, token) if errors.Contains(err, ErrExpiry) { err = svc.keys.Remove(ctx, key.Issuer, key.ID) return Key{}, errors.Wrap(svcerr.ErrAuthentication, errors.Wrap(ErrKeyExpired, err)) } if err != nil { return Key{}, errors.Wrap(svcerr.ErrAuthentication, errors.Wrap(errIdentify, err)) } switch key.Type { case PersonalAccessToken: res, err := svc.IdentifyPAT(ctx, token) if err != nil { return Key{}, err } return Key{ID: res.ID, Type: PersonalAccessToken, Subject: res.User, Role: res.Role}, nil case RecoveryKey, AccessKey, InvitationKey, RefreshKey: return key, nil case APIKey: _, err := svc.keys.Retrieve(ctx, key.Issuer, key.ID) if err != nil { return Key{}, svcerr.ErrAuthentication } return key, nil default: return Key{}, svcerr.ErrAuthentication } } func (svc service) RetrieveJWKS() []PublicKeyInfo { keys, err := svc.tokenizer.RetrieveJWKS() if err != nil { return nil } return keys } func (svc service) ListUserRefreshTokens(ctx context.Context, userID string) ([]TokenInfo, error) { tokenInfo, err := svc.tokensCache.ListUserTokens(ctx, userID) if err != nil { return nil, errors.Wrap(errListRefreshKeys, err) } return tokenInfo, nil } func (svc service) Authorize(ctx context.Context, pr policies.Policy, patAuthz *PATAuthz) error { if patAuthz != nil { if err := svc.AuthorizePAT(ctx, patAuthz.UserID, patAuthz.PatID, patAuthz.EntityType, patAuthz.Domain, patAuthz.Operation, patAuthz.EntityID); err != nil { return err } } if err := svc.PolicyValidation(pr); err != nil { return errors.Wrap(svcerr.ErrMalformedEntity, err) } if err := svc.checkPolicy(ctx, pr); err != nil { return err } return nil } func (svc service) checkPolicy(ctx context.Context, pr policies.Policy) error { if err := svc.evaluator.CheckPolicy(ctx, pr); err != nil { return errors.Wrap(svcerr.ErrAuthorization, err) } return nil } func (svc service) PolicyValidation(pr policies.Policy) error { if pr.ObjectType == policies.PlatformType && pr.Object != policies.MagistralaObject { return errPlatform } return nil } func (svc service) tmpKey(ctx context.Context, duration time.Duration, key Key) (Token, error) { key.ExpiresAt = time.Now().UTC().Add(duration) if err := svc.checkUserRole(ctx, key); err != nil { return Token{}, errors.Wrap(errIssueTmp, err) } value, err := svc.tokenizer.Issue(key) if err != nil { return Token{}, errors.Wrap(errIssueTmp, err) } return Token{AccessToken: value}, nil } func (svc service) accessKey(ctx context.Context, key Key) (Token, error) { var err error key.Type = AccessKey key.ExpiresAt = time.Now().UTC().Add(svc.loginDuration) if err := svc.checkUserRole(ctx, key); err != nil { return Token{}, errors.Wrap(errIssueUser, err) } access, err := svc.tokenizer.Issue(key) if err != nil { return Token{}, errors.Wrap(errIssueTmp, err) } key.ExpiresAt = time.Now().UTC().Add(svc.refreshDuration) key.Type = RefreshKey id, err := svc.idProvider.ID() if err != nil { return Token{}, errors.Wrap(errIssueTmp, err) } key.ID = id refresh, err := svc.tokenizer.Issue(key) if err != nil { return Token{}, errors.Wrap(errIssueTmp, err) } if key.Subject != "" && key.ExpiresAt.After(time.Now()) { if err := svc.tokensCache.SaveActive(ctx, key.Subject, key.ID, key.Description, key.ExpiresAt); err != nil { return Token{}, errors.Wrap(errSaveRefreshKey, err) } } return Token{AccessToken: access, RefreshToken: refresh}, nil } func (svc service) invitationKey(ctx context.Context, key Key) (Token, error) { var err error key.Type = InvitationKey key.ExpiresAt = time.Now().UTC().Add(svc.invitationDuration) if err := svc.checkUserRole(ctx, key); err != nil { return Token{}, errors.Wrap(errIssueTmp, err) } access, err := svc.tokenizer.Issue(key) if err != nil { return Token{}, errors.Wrap(errIssueTmp, err) } return Token{AccessToken: access}, nil } func (svc service) refreshKey(ctx context.Context, token string, key Key) (Token, error) { k, err := svc.tokenizer.Parse(ctx, token) if err != nil { return Token{}, errors.Wrap(svcerr.ErrAuthentication, err) } if k.Type != RefreshKey { return Token{}, errIssueUser } ok, err := svc.tokensCache.IsActive(ctx, k.ID) if err != nil { return Token{}, errors.Wrap(svcerr.ErrViewEntity, err) } if !ok { return Token{}, ErrRevokedToken } key.ID = k.ID key.Type = AccessKey key.Subject = k.Subject if err := svc.checkUserRole(ctx, key); err != nil { return Token{}, errors.Wrap(errIssueUser, err) } key.Role = k.Role key.ExpiresAt = time.Now().UTC().Add(svc.loginDuration) access, err := svc.tokenizer.Issue(key) if err != nil { return Token{}, errors.Wrap(errIssueTmp, err) } key.ExpiresAt = k.ExpiresAt key.Type = RefreshKey refresh, err := svc.tokenizer.Issue(key) if err != nil { return Token{}, errors.Wrap(errIssueTmp, err) } if key.Subject != "" && key.ExpiresAt.After(time.Now()) { if err := svc.tokensCache.SaveActive(ctx, key.Subject, key.ID, key.Description, key.ExpiresAt); err != nil { return Token{}, errors.Wrap(errSaveRefreshKey, err) } } return Token{AccessToken: access, RefreshToken: refresh}, nil } func (svc service) checkUserRole(ctx context.Context, key Key) (err error) { switch key.Role { case AdminRole: if err = svc.Authorize(ctx, policies.Policy{ Subject: key.Subject, SubjectType: policies.UserType, Permission: policies.AdminPermission, Object: policies.MagistralaObject, ObjectType: policies.PlatformType, }, nil); err != nil { return errRoleAuth } return nil case UserRole: if err = svc.Authorize(ctx, policies.Policy{ Subject: key.Subject, SubjectType: policies.UserType, Permission: policies.MembershipPermission, Object: policies.MagistralaObject, ObjectType: policies.PlatformType, }, nil); err != nil { return errRoleAuth } return nil default: return nil } } func (svc service) getUserRole(ctx context.Context, userID string) (role Role) { rl := UserRole if err := svc.Authorize(ctx, policies.Policy{ Subject: userID, SubjectType: policies.UserType, Permission: policies.AdminPermission, Object: policies.MagistralaObject, ObjectType: policies.PlatformType, }, nil); err == nil { rl = AdminRole } return rl } func (svc service) userKey(ctx context.Context, token string, key Key) (Token, error) { id, sub, err := svc.authenticate(ctx, token) if err != nil { return Token{}, errors.Wrap(errIssueUser, err) } key.Issuer = id if key.Subject == "" { key.Subject = sub } if err := svc.checkUserRole(ctx, key); err != nil { return Token{}, errors.Wrap(errIssueUser, err) } keyID, err := svc.idProvider.ID() if err != nil { return Token{}, errors.Wrap(errIssueUser, err) } key.ID = keyID if _, err := svc.keys.Save(ctx, key); err != nil { return Token{}, errors.Wrap(errIssueUser, err) } tkn, err := svc.tokenizer.Issue(key) if err != nil { return Token{}, errors.Wrap(errIssueUser, err) } return Token{AccessToken: tkn}, nil } func (svc service) authenticate(ctx context.Context, token string) (string, string, error) { key, err := svc.tokenizer.Parse(ctx, token) if err != nil { return "", "", errors.Wrap(svcerr.ErrAuthentication, err) } // Only login key token is valid for login. if key.Type != AccessKey || key.Issuer == "" { return "", "", svcerr.ErrAuthentication } return key.Issuer, key.Subject, nil } // Switch the relative permission for the relation. func SwitchToPermission(relation string) string { switch relation { case policies.AdministratorRelation: return policies.AdminPermission case policies.EditorRelation: return policies.EditPermission case policies.ContributorRelation: return policies.ViewPermission case policies.MemberRelation: return policies.MembershipPermission case policies.GuestRelation: return policies.ViewPermission default: return relation } } func EncodeDomainUserID(domainID, userID string) string { if domainID == "" || userID == "" { return "" } return domainID + "_" + userID } func DecodeDomainUserID(domainUserID string) (string, string) { if domainUserID == "" { return domainUserID, domainUserID } duid := strings.Split(domainUserID, "_") switch { case len(duid) == 2: return duid[0], duid[1] case len(duid) == 1: return duid[0], "" case len(duid) == 0 || len(duid) > 2: fallthrough default: return "", "" } } func (svc service) CreatePAT(ctx context.Context, token, name, description string, duration time.Duration) (PAT, error) { key, err := svc.Identify(ctx, token) if err != nil { return PAT{}, err } id, err := svc.idProvider.ID() if err != nil { return PAT{}, errors.Wrap(svcerr.ErrCreateEntity, err) } secret, hash, err := svc.generateSecretAndHash(key.Subject, id) if err != nil { return PAT{}, errors.Wrap(svcerr.ErrCreateEntity, err) } now := time.Now().UTC() pat := PAT{ ID: id, User: key.Subject, Name: name, Description: description, Secret: hash, IssuedAt: now, ExpiresAt: now.Add(duration), Status: ActiveStatus, Revoked: false, } if err := pat.Validate(); err != nil { return PAT{}, errors.Wrap(svcerr.ErrCreateEntity, err) } if err := svc.pats.Save(ctx, pat); err != nil { return PAT{}, errors.Wrap(errCreatePAT, err) } pat.Secret = secret return pat, nil } func (svc service) UpdatePATName(ctx context.Context, token, patID, name string) (PAT, error) { key, err := svc.authnAuthzUserPAT(ctx, token, patID) if err != nil { return PAT{}, err } pat, err := svc.pats.UpdateName(ctx, key.Subject, patID, name) if err != nil { return PAT{}, errors.Wrap(errUpdatePAT, err) } return pat, nil } func (svc service) UpdatePATDescription(ctx context.Context, token, patID, description string) (PAT, error) { key, err := svc.authnAuthzUserPAT(ctx, token, patID) if err != nil { return PAT{}, err } pat, err := svc.pats.UpdateDescription(ctx, key.Subject, patID, description) if err != nil { return PAT{}, errors.Wrap(errUpdatePAT, err) } return pat, nil } func (svc service) RetrievePAT(ctx context.Context, token, patID string) (PAT, error) { key, err := svc.Identify(ctx, token) if err != nil { return PAT{}, err } pat, err := svc.pats.Retrieve(ctx, key.Subject, patID) if err != nil { return PAT{}, errors.Wrap(errRetrievePAT, err) } return pat, nil } func (svc service) ListPATS(ctx context.Context, token string, pm PATSPageMeta) (PATSPage, error) { key, err := svc.Identify(ctx, token) if err != nil { return PATSPage{}, err } patsPage, err := svc.pats.RetrieveAll(ctx, key.Subject, pm) if err != nil { return PATSPage{}, errors.Wrap(errRetrievePAT, err) } return patsPage, nil } func (svc service) DeletePAT(ctx context.Context, token, patID string) error { key, err := svc.authnAuthzUserPAT(ctx, token, patID) if err != nil { return err } if err := svc.pats.Remove(ctx, key.Subject, patID); err != nil { return errors.Wrap(errDeletePAT, err) } return nil } func (svc service) ResetPATSecret(ctx context.Context, token, patID string, duration time.Duration) (PAT, error) { key, err := svc.authnAuthzUserPAT(ctx, token, patID) if err != nil { return PAT{}, err } // Generate new HashToken take place here secret, hash, err := svc.generateSecretAndHash(key.Subject, patID) if err != nil { return PAT{}, errors.Wrap(svcerr.ErrUpdateEntity, err) } pat, err := svc.pats.UpdateTokenHash(ctx, key.Subject, patID, hash, time.Now().UTC().Add(duration)) if err != nil { return PAT{}, errors.Wrap(svcerr.ErrUpdateEntity, err) } if err := svc.pats.Reactivate(ctx, key.Subject, patID); err != nil { return PAT{}, errors.Wrap(svcerr.ErrUpdateEntity, err) } pat.Secret = secret pat.Status = ActiveStatus pat.RevokedAt = time.Time{} return pat, nil } func (svc service) RevokePATSecret(ctx context.Context, token, patID string) error { key, err := svc.authnAuthzUserPAT(ctx, token, patID) if err != nil { return err } if err := svc.pats.Revoke(ctx, key.Subject, patID); err != nil { return errors.Wrap(svcerr.ErrUpdateEntity, err) } return nil } func (svc service) RemoveAllPAT(ctx context.Context, token string) error { key, err := svc.Identify(ctx, token) if err != nil { return err } if err := svc.pats.RemoveAllPAT(ctx, key.Subject); err != nil { return errors.Wrap(svcerr.ErrRemoveEntity, err) } return nil } func (svc service) AddScope(ctx context.Context, token, patID string, scopes []Scope) error { key, err := svc.authnAuthzUserPAT(ctx, token, patID) if err != nil { return err } for i := range len(scopes) { scopes[i].ID, err = svc.idProvider.ID() if err != nil { return errors.Wrap(svcerr.ErrCreateEntity, err) } scopes[i].PatID = patID } err = svc.pats.AddScope(ctx, key.Subject, scopes) if err != nil { return errors.Wrap(svcerr.ErrCreateEntity, err) } return nil } func (svc service) RemoveScope(ctx context.Context, token, patID string, scopesIDs ...string) error { key, err := svc.authnAuthzUserPAT(ctx, token, patID) if err != nil { return err } err = svc.pats.RemoveScope(ctx, key.Subject, scopesIDs...) if err != nil { return errors.Wrap(svcerr.ErrRemoveEntity, err) } return nil } func (svc service) ListScopes(ctx context.Context, token string, pm ScopesPageMeta) (ScopesPage, error) { _, err := svc.authnAuthzUserPAT(ctx, token, pm.PatID) if err != nil { return ScopesPage{}, err } patsPage, err := svc.pats.RetrieveScope(ctx, pm) if err != nil { return ScopesPage{}, errors.Wrap(errRetrievePAT, err) } return patsPage, nil } func (svc service) RemovePATAllScope(ctx context.Context, token, patID string) error { _, err := svc.authnAuthzUserPAT(ctx, token, patID) if err != nil { return err } if err := svc.pats.RemoveAllScope(ctx, patID); err != nil { return errors.Wrap(svcerr.ErrRemoveEntity, err) } return nil } func (svc service) IdentifyPAT(ctx context.Context, secret string) (PAT, error) { parts := strings.Split(secret, patSecretSeparator) if len(parts) != 3 && parts[0] != patPrefix { return PAT{}, errors.Wrap(svcerr.ErrAuthentication, errMalformedPAT) } userID, patID, err := decode(parts[1]) if err != nil { return PAT{}, errors.Wrap(svcerr.ErrAuthentication, errMalformedPAT) } secretHash, revoked, expired, err := svc.pats.RetrieveSecretAndRevokeStatus(ctx, userID.String(), patID.String()) if err != nil { return PAT{}, errors.Wrap(svcerr.ErrAuthentication, err) } if revoked { return PAT{}, errors.Wrap(svcerr.ErrAuthentication, errRevokedPAT) } if expired { return PAT{}, errors.Wrap(svcerr.ErrAuthentication, ErrExpiry) } if err := svc.hasher.Compare(secret, secretHash); err != nil { return PAT{}, errors.Wrap(svcerr.ErrAuthentication, err) } role := svc.getUserRole(ctx, userID.String()) pat := PAT{ID: patID.String(), User: userID.String(), Role: role} return pat, nil } func (svc service) AuthorizePAT(ctx context.Context, userID, patID string, entityType EntityType, domainID string, operation string, entityID string) error { if err := svc.pats.CheckScope(ctx, userID, patID, entityType, domainID, operation, entityID); err != nil { return errors.Wrap(svcerr.ErrAuthorization, err) } return nil } func (svc service) generateSecretAndHash(userID, patID string) (string, string, error) { uID, err := uuid.Parse(userID) if err != nil { return "", "", errors.Wrap(errFailedToParseUUID, err) } pID, err := uuid.Parse(patID) if err != nil { return "", "", errors.Wrap(errFailedToParseUUID, err) } randomPart, err := generateRandomString(100) if err != nil { return "", "", err } secret := patPrefix + patSecretSeparator + encode(uID, pID) + patSecretSeparator + randomPart secretHash, err := svc.hasher.Hash(secret) return secret, secretHash, err } func encode(userID, patID uuid.UUID) string { c := append(userID[:], patID[:]...) return base64.StdEncoding.EncodeToString(c) } func decode(encoded string) (uuid.UUID, uuid.UUID, error) { data, err := base64.StdEncoding.DecodeString(encoded) if err != nil { return uuid.Nil, uuid.Nil, err } if len(data) != 32 { return uuid.Nil, uuid.Nil, errInvalidLenFor2UUIDs } var userID, patID uuid.UUID copy(userID[:], data[:16]) copy(patID[:], data[16:]) return userID, patID, nil } func generateRandomString(n int) (string, error) { letterRunes := []rune(randStr) b := make([]rune, n) randBytes := make([]byte, n) if _, err := rand.Read(randBytes); err != nil { return "", errors.Wrap(errors.New("failed to generate random string"), err) } for i := range b { b[i] = letterRunes[int(randBytes[i])%len(letterRunes)] } return string(b), nil } func (svc service) authnAuthzUserPAT(ctx context.Context, token, patID string) (Key, error) { key, err := svc.Identify(ctx, token) if err != nil { return Key{}, err } _, err = svc.pats.Retrieve(ctx, key.Subject, patID) if err != nil { if errors.Contains(err, repoerr.ErrNotFound) { return Key{}, svcerr.ErrNotFound } return Key{}, errors.Wrap(svcerr.ErrAuthorization, err) } return key, nil }