feat: User login commands (#1228)

* feat: change login to command side

* feat: change login to command side

* fix: fix push on user

* feat: user command side

* feat: sign out

* feat: command side login

* feat: command side login

* feat: fix register user

* feat: fix register user

* feat: fix web auth n events

* feat: add machine keys

* feat: send codes

* feat: move authrequest to domain

* feat: move authrequest to domain

* feat: webauthn working

* feat: external users

* feat: external users login

* feat: notify users

* fix: tests

* feat: cascade remove user grants on project remove

* fix: webauthn

* fix: pr requests

* fix: register human with member

* fix: fix bugs

* fix: fix bugs
This commit is contained in:
Fabi
2021-02-08 11:30:30 +01:00
committed by GitHub
parent c65331df1a
commit 320679467b
123 changed files with 2949 additions and 1212 deletions

View File

@@ -0,0 +1,150 @@
package domain
import (
"github.com/caos/zitadel/internal/errors"
"golang.org/x/text/language"
"strings"
"time"
)
type AuthRequest struct {
ID string
AgentID string
CreationDate time.Time
ChangeDate time.Time
BrowserInfo *BrowserInfo
ApplicationID string
CallbackURI string
TransferState string
Prompt Prompt
PossibleLOAs []LevelOfAssurance
UiLocales []string
LoginHint string
MaxAuthAge uint32
Request Request
levelOfAssurance LevelOfAssurance
UserID string
LoginName string
DisplayName string
UserOrgID string
RequestedOrgID string
RequestedOrgName string
SelectedIDPConfigID string
LinkingUsers []*ExternalUser
PossibleSteps []NextStep
PasswordVerified bool
MFAsVerified []MFAType
Audience []string
AuthTime time.Time
Code string
LoginPolicy *LoginPolicy
AllowedExternalIDPs []*IDPProvider
}
type ExternalUser struct {
IDPConfigID string
ExternalUserID string
DisplayName string
PreferredUsername string
FirstName string
LastName string
NickName string
Email string
IsEmailVerified bool
PreferredLanguage language.Tag
Phone string
IsPhoneVerified bool
}
type Prompt int32
const (
PromptUnspecified Prompt = iota
PromptNone
PromptLogin
PromptConsent
PromptSelectAccount
)
type LevelOfAssurance int
const (
LevelOfAssuranceNone LevelOfAssurance = iota
)
type MFAType int
const (
MFATypeOTP MFAType = iota
MFATypeU2F
MFATypeU2FUserVerification
)
type MFALevel int
const (
MFALevelNotSetUp MFALevel = iota
MFALevelSecondFactor
MFALevelMultiFactor
MFALevelMultiFactorCertified
)
func NewAuthRequestFromType(requestType AuthRequestType) (*AuthRequest, error) {
request, ok := authRequestTypeMapping[requestType]
if !ok {
return nil, errors.ThrowInvalidArgument(nil, "DOMAIN-ds2kl", "invalid request type")
}
return &AuthRequest{Request: request}, nil
}
func (a *AuthRequest) WithCurrentInfo(info *BrowserInfo) *AuthRequest {
a.BrowserInfo = info
return a
}
func (a *AuthRequest) SetUserInfo(userID, loginName, displayName, userOrgID string) {
a.UserID = userID
a.LoginName = loginName
a.DisplayName = displayName
a.UserOrgID = userOrgID
}
func (a *AuthRequest) MFALevel() MFALevel {
return -1
//PLANNED: check a.PossibleLOAs (and Prompt Login?)
}
func (a *AuthRequest) AppendAudIfNotExisting(aud string) {
for _, a := range a.Audience {
if a == aud {
return
}
}
a.Audience = append(a.Audience, aud)
}
func (a *AuthRequest) GetScopeProjectIDsForAud() []string {
projectIDs := make([]string, 0)
switch request := a.Request.(type) {
case *AuthRequestOIDC:
for _, scope := range request.Scopes {
if strings.HasPrefix(scope, ProjectIDScope) && strings.HasSuffix(scope, AudSuffix) {
projectIDs = append(projectIDs, strings.TrimSuffix(strings.TrimPrefix(scope, ProjectIDScope), AudSuffix))
}
}
}
return projectIDs
}
func (a *AuthRequest) GetScopeOrgPrimaryDomain() string {
switch request := a.Request.(type) {
case *AuthRequestOIDC:
for _, scope := range request.Scopes {
if strings.HasPrefix(scope, OrgDomainPrimaryScope) {
return strings.TrimPrefix(scope, OrgDomainPrimaryScope)
}
}
}
return ""
}

View File

@@ -0,0 +1,22 @@
package domain
import (
"net"
net_http "net/http"
http_util "github.com/caos/zitadel/internal/api/http"
)
type BrowserInfo struct {
UserAgent string
AcceptLanguage string
RemoteIP net.IP
}
func BrowserInfoFromRequest(r *net_http.Request) *BrowserInfo {
return &BrowserInfo{
UserAgent: r.Header.Get(http_util.UserAgentHeader),
AcceptLanguage: r.Header.Get(http_util.AcceptLanguage),
RemoteIP: http_util.RemoteIPFromRequest(r),
}
}

View File

@@ -60,15 +60,15 @@ func (u *Human) IsValid() bool {
return u.Profile != nil && u.FirstName != "" && u.LastName != "" && u.Email != nil && u.Email.IsValid() && u.Phone == nil || (u.Phone != nil && u.Phone.PhoneNumber != "" && u.Phone.IsValid())
}
func (u *Human) CheckOrgIAMPolicy(userName string, policy *OrgIAMPolicy) error {
func (u *Human) CheckOrgIAMPolicy(policy *OrgIAMPolicy) error {
if policy == nil {
return caos_errors.ThrowPreconditionFailed(nil, "DOMAIN-zSH7j", "Errors.Users.OrgIamPolicyNil")
}
if policy.UserLoginMustBeDomain && strings.Contains(userName, "@") {
if policy.UserLoginMustBeDomain && strings.Contains(u.Username, "@") {
return caos_errors.ThrowPreconditionFailed(nil, "DOMAIN-se4sJ", "Errors.User.EmailAsUsernameNotAllowed")
}
if !policy.UserLoginMustBeDomain && u.Profile != nil && userName == "" && u.Email != nil {
userName = u.EmailAddress
if !policy.UserLoginMustBeDomain && u.Profile != nil && u.Username == "" && u.Email != nil {
u.Username = u.EmailAddress
}
return nil
}

View File

@@ -1,6 +1,9 @@
package domain
import es_models "github.com/caos/zitadel/internal/eventstore/models"
import (
"bytes"
es_models "github.com/caos/zitadel/internal/eventstore/models"
)
type WebAuthNToken struct {
es_models.ObjectRoot
@@ -55,3 +58,12 @@ func GetTokenToVerify(tokens []*WebAuthNToken) (int, *WebAuthNToken) {
}
return -1, nil
}
func GetTokenByKeyID(tokens []*WebAuthNToken, keyID []byte) (int, *WebAuthNToken) {
for i, token := range tokens {
if bytes.Compare(token.KeyID, keyID) == 0 {
return i, token
}
}
return -1, nil
}

View File

@@ -89,3 +89,12 @@ const (
func (f IDPConfigStylingType) Valid() bool {
return f >= 0 && f < idpConfigStylingTypeCount
}
func (st IDPConfigStylingType) GetCSSClass() string {
switch st {
case IDPConfigStylingTypeGoogle:
return "google"
default:
return ""
}
}

View File

@@ -1,5 +1,21 @@
package domain
import (
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/eventstore/models"
"time"
)
type MachineKey struct {
models.ObjectRoot
KeyID string
Type MachineKeyType
ExpirationDate time.Time
PrivateKey []byte
PublicKey []byte
}
type MachineKeyType int32
const (
@@ -9,6 +25,33 @@ const (
keyCount
)
type MachineKeyState int32
const (
MachineKeyStateUnspecified MachineKeyState = iota
MachineKeyStateActive
MachineKeyStateRemoved
machineKeyStateCount
)
func (f MachineKeyState) Valid() bool {
return f >= 0 && f < machineKeyStateCount
}
func (f MachineKeyType) Valid() bool {
return f >= 0 && f < keyCount
}
func (key *MachineKey) GenerateNewMachineKeyPair(keySize int) error {
privateKey, publicKey, err := crypto.GenerateKeyPair(keySize)
if err != nil {
return err
}
key.PublicKey, err = crypto.PublicKeyToBytes(publicKey)
if err != nil {
return err
}
key.PrivateKey = crypto.PrivateKeyToBytes(privateKey)
return nil
}

View File

@@ -0,0 +1,149 @@
package domain
type NextStep interface {
Type() NextStepType
}
type NextStepType int32
const (
NextStepUnspecified NextStepType = iota
NextStepLogin
NextStepUserSelection
NextStepInitUser
NextStepPassword
NextStepChangePassword
NextStepInitPassword
NextStepVerifyEmail
NextStepMFAPrompt
NextStepMFAVerify
NextStepRedirectToCallback
NextStepChangeUsername
NextStepLinkUsers
NextStepExternalNotFoundOption
NextStepExternalLogin
NextStepGrantRequired
NextStepPasswordless
)
type LoginStep struct{}
func (s *LoginStep) Type() NextStepType {
return NextStepLogin
}
type SelectUserStep struct {
Users []UserSelection
}
func (s *SelectUserStep) Type() NextStepType {
return NextStepUserSelection
}
type UserSelection struct {
UserID string
DisplayName string
LoginName string
UserSessionState UserSessionState
SelectionPossible bool
}
type UserSessionState int32
const (
UserSessionStateActive UserSessionState = iota
UserSessionStateTerminated
)
type InitUserStep struct {
PasswordSet bool
}
func (s *InitUserStep) Type() NextStepType {
return NextStepInitUser
}
type ExternalNotFoundOptionStep struct{}
func (s *ExternalNotFoundOptionStep) Type() NextStepType {
return NextStepExternalNotFoundOption
}
type PasswordStep struct{}
func (s *PasswordStep) Type() NextStepType {
return NextStepPassword
}
type ExternalLoginStep struct {
SelectedIDPConfigID string
}
func (s *ExternalLoginStep) Type() NextStepType {
return NextStepExternalLogin
}
type PasswordlessStep struct{}
func (s *PasswordlessStep) Type() NextStepType {
return NextStepPasswordless
}
type ChangePasswordStep struct{}
func (s *ChangePasswordStep) Type() NextStepType {
return NextStepChangePassword
}
type InitPasswordStep struct{}
func (s *InitPasswordStep) Type() NextStepType {
return NextStepInitPassword
}
type ChangeUsernameStep struct{}
func (s *ChangeUsernameStep) Type() NextStepType {
return NextStepChangeUsername
}
type VerifyEMailStep struct{}
func (s *VerifyEMailStep) Type() NextStepType {
return NextStepVerifyEmail
}
type MFAPromptStep struct {
Required bool
MFAProviders []MFAType
}
func (s *MFAPromptStep) Type() NextStepType {
return NextStepMFAPrompt
}
type MFAVerificationStep struct {
MFAProviders []MFAType
}
func (s *MFAVerificationStep) Type() NextStepType {
return NextStepMFAVerify
}
type LinkUsersStep struct{}
func (s *LinkUsersStep) Type() NextStepType {
return NextStepLinkUsers
}
type GrantRequiredStep struct{}
func (s *GrantRequiredStep) Type() NextStepType {
return NextStepGrantRequired
}
type RedirectToCallbackStep struct{}
func (s *RedirectToCallbackStep) Type() NextStepType {
return NextStepRedirectToCallback
}

View File

@@ -0,0 +1,17 @@
package domain
type OIDCCodeChallenge struct {
Challenge string
Method OIDCCodeChallengeMethod
}
func (c *OIDCCodeChallenge) IsValid() bool {
return c.Challenge != ""
}
type OIDCCodeChallengeMethod int32
const (
CodeChallengeMethodPlain OIDCCodeChallengeMethod = iota
CodeChallengeMethodS256
)

View File

@@ -8,7 +8,7 @@ type LoginPolicy struct {
Default bool
AllowUsernamePassword bool
AllowRegister bool
AllowExternalIdp bool
AllowExternalIDP bool
IDPProviders []*IDPProvider
ForceMFA bool
SecondFactors []SecondFactorType
@@ -20,6 +20,11 @@ type IDPProvider struct {
models.ObjectRoot
Type IdentityProviderType
IDPConfigID string
Name string
StylingType IDPConfigStylingType
IDPConfigType IDPConfigType
IDPState IDPConfigState
}
type PasswordlessType int32
@@ -34,3 +39,11 @@ const (
func (f PasswordlessType) Valid() bool {
return f >= 0 && f < passwordlessCount
}
func (p *LoginPolicy) HasSecondFactors() bool {
return len(p.SecondFactors) > 0
}
func (p *LoginPolicy) HasMultiFactors() bool {
return len(p.MultiFactors) > 0
}

View File

@@ -0,0 +1,54 @@
package domain
const (
OrgDomainPrimaryScope = "urn:zitadel:iam:org:domain:primary:"
OrgDomainPrimaryClaim = "urn:zitadel:iam:org:domain:primary"
ProjectIDScope = "urn:zitadel:iam:org:project:id:"
AudSuffix = ":aud"
)
//TODO: Change AuthRequest to interface and let oidcauthreqesut implement it
type Request interface {
Type() AuthRequestType
IsValid() bool
}
type AuthRequestType int32
var (
authRequestTypeMapping = map[AuthRequestType]Request{
AuthRequestTypeOIDC: &AuthRequestOIDC{},
}
)
const (
AuthRequestTypeOIDC AuthRequestType = iota
AuthRequestTypeSAML
)
type AuthRequestOIDC struct {
Scopes []string
ResponseType OIDCResponseType
Nonce string
CodeChallenge *OIDCCodeChallenge
}
func (a *AuthRequestOIDC) Type() AuthRequestType {
return AuthRequestTypeOIDC
}
func (a *AuthRequestOIDC) IsValid() bool {
return len(a.Scopes) > 0 &&
a.CodeChallenge == nil || a.CodeChallenge != nil && a.CodeChallenge.IsValid()
}
type AuthRequestSAML struct {
}
func (a *AuthRequestSAML) Type() AuthRequestType {
return AuthRequestTypeSAML
}
func (a *AuthRequestSAML) IsValid() bool {
return true
}

View File

@@ -2,6 +2,7 @@ package domain
const (
RoleOrgOwner = "ORG_OWNER"
RoleOrgProjectCreator = "ORG_PROJECT_CREATOR"
RoleIAMOwner = "IAM_OWNER"
RoleProjectOwner = "PROJECT_OWNER"
RoleProjectOwnerGlobal = "PROJECT_OWNER_GLOBAL"

View File

@@ -0,0 +1,18 @@
package domain
import (
es_models "github.com/caos/zitadel/internal/eventstore/models"
"time"
)
type Token struct {
es_models.ObjectRoot
TokenID string
ApplicationID string
UserAgentID string
Audience []string
Expiration time.Time
Scopes []string
PreferredLanguage string
}