mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 01:15:52 +08:00
feat: Just-in-time provisioning of SSO users (#3394)
* feat: auto provisioning of SSO users rather than needing invite link to login each user * updating errors Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com> * fix: set IsUser: true when SSO feature is available * fix: signoz login from IDP (#3396) * fix: enable login from IDP with relayState set with domainName * update comments on function Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com> * chore: added error checks to fetch domain from SAML relay state --------- Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com> --------- Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
This commit is contained in:
parent
ee6b290a0c
commit
591ea96285
@ -113,7 +113,7 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if domain != nil && domain.SsoEnabled {
|
if domain != nil && domain.SsoEnabled {
|
||||||
// so is enabled, create user and respond precheck data
|
// sso is enabled, create user and respond precheck data
|
||||||
user, apierr := baseauth.RegisterInvitedUser(ctx, req, true)
|
user, apierr := baseauth.RegisterInvitedUser(ctx, req, true)
|
||||||
if apierr != nil {
|
if apierr != nil {
|
||||||
RespondError(w, apierr, nil)
|
RespondError(w, apierr, nil)
|
||||||
|
@ -5,16 +5,61 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"go.signoz.io/signoz/ee/query-service/constants"
|
"go.signoz.io/signoz/ee/query-service/constants"
|
||||||
"go.signoz.io/signoz/ee/query-service/model"
|
"go.signoz.io/signoz/ee/query-service/model"
|
||||||
|
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
|
||||||
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
|
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
|
||||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||||
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
|
"go.signoz.io/signoz/pkg/query-service/utils"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrepareSsoRedirect prepares redirect page link after SSO response
|
func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (*basemodel.User, basemodel.BaseApiError) {
|
||||||
|
// get auth domain from email domain
|
||||||
|
domain, apierr := m.GetDomainByEmail(ctx, email)
|
||||||
|
|
||||||
|
if apierr != nil {
|
||||||
|
zap.S().Errorf("failed to get domain from email", apierr)
|
||||||
|
return nil, model.InternalErrorStr("failed to get domain from email")
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := baseauth.PasswordHash(utils.GeneratePassowrd())
|
||||||
|
if err != nil {
|
||||||
|
zap.S().Errorf("failed to generate password hash when registering a user via SSO redirect", zap.Error(err))
|
||||||
|
return nil, model.InternalErrorStr("failed to generate password hash")
|
||||||
|
}
|
||||||
|
|
||||||
|
group, apiErr := m.GetGroupByName(ctx, baseconst.ViewerGroup)
|
||||||
|
if apiErr != nil {
|
||||||
|
zap.S().Debugf("GetGroupByName failed, err: %v\n", apiErr.Err)
|
||||||
|
return nil, apiErr
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &basemodel.User{
|
||||||
|
Id: uuid.NewString(),
|
||||||
|
Name: "",
|
||||||
|
Email: email,
|
||||||
|
Password: hash,
|
||||||
|
CreatedAt: time.Now().Unix(),
|
||||||
|
ProfilePictureURL: "", // Currently unused
|
||||||
|
GroupId: group.Id,
|
||||||
|
OrgId: domain.OrgId,
|
||||||
|
}
|
||||||
|
|
||||||
|
user, apiErr = m.CreateUser(ctx, user, false)
|
||||||
|
if apiErr != nil {
|
||||||
|
zap.S().Debugf("CreateUser failed, err: %v\n", apiErr.Err)
|
||||||
|
return nil, apiErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepareSsoRedirect prepares redirect page link after SSO response
|
||||||
// is successfully parsed (i.e. valid email is available)
|
// is successfully parsed (i.e. valid email is available)
|
||||||
func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email string) (redirectURL string, apierr basemodel.BaseApiError) {
|
func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email string) (redirectURL string, apierr basemodel.BaseApiError) {
|
||||||
|
|
||||||
@ -24,7 +69,20 @@ func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email st
|
|||||||
return "", model.BadRequestStr("invalid user email received from the auth provider")
|
return "", model.BadRequestStr("invalid user email received from the auth provider")
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenStore, err := baseauth.GenerateJWTForUser(&userPayload.User)
|
user := &basemodel.User{}
|
||||||
|
|
||||||
|
if userPayload == nil {
|
||||||
|
newUser, apiErr := m.createUserForSAMLRequest(ctx, email)
|
||||||
|
user = newUser
|
||||||
|
if apiErr != nil {
|
||||||
|
zap.S().Errorf("failed to create user with email received from auth provider: %v", apierr.Error())
|
||||||
|
return "", apiErr
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
user = &userPayload.User
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenStore, err := baseauth.GenerateJWTForUser(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.S().Errorf("failed to generate token for SSO login user", err)
|
zap.S().Errorf("failed to generate token for SSO login user", err)
|
||||||
return "", model.InternalErrorStr("failed to generate token for the user")
|
return "", model.InternalErrorStr("failed to generate token for the user")
|
||||||
@ -33,7 +91,7 @@ func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email st
|
|||||||
return fmt.Sprintf("%s?jwt=%s&usr=%s&refreshjwt=%s",
|
return fmt.Sprintf("%s?jwt=%s&usr=%s&refreshjwt=%s",
|
||||||
redirectUri,
|
redirectUri,
|
||||||
tokenStore.AccessJwt,
|
tokenStore.AccessJwt,
|
||||||
userPayload.User.Id,
|
user.Id,
|
||||||
tokenStore.RefreshJwt), nil
|
tokenStore.RefreshJwt), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,6 +134,7 @@ func (m *modelDao) PrecheckLogin(ctx context.Context, email, sourceUrl string) (
|
|||||||
if userPayload == nil {
|
if userPayload == nil {
|
||||||
resp.IsUser = false
|
resp.IsUser = false
|
||||||
}
|
}
|
||||||
|
|
||||||
ssoAvailable := true
|
ssoAvailable := true
|
||||||
err := m.checkFeature(model.SSO)
|
err := m.checkFeature(model.SSO)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -91,6 +150,8 @@ func (m *modelDao) PrecheckLogin(ctx context.Context, email, sourceUrl string) (
|
|||||||
|
|
||||||
if ssoAvailable {
|
if ssoAvailable {
|
||||||
|
|
||||||
|
resp.IsUser = true
|
||||||
|
|
||||||
// find domain from email
|
// find domain from email
|
||||||
orgDomain, apierr := m.GetDomainByEmail(ctx, email)
|
orgDomain, apierr := m.GetDomainByEmail(ctx, email)
|
||||||
if apierr != nil {
|
if apierr != nil {
|
||||||
|
@ -4,8 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/url"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -28,29 +28,70 @@ type StoredDomain struct {
|
|||||||
|
|
||||||
// GetDomainFromSsoResponse uses relay state received from IdP to fetch
|
// GetDomainFromSsoResponse uses relay state received from IdP to fetch
|
||||||
// user domain. The domain is further used to process validity of the response.
|
// user domain. The domain is further used to process validity of the response.
|
||||||
// when sending login request to IdP we send relay state as URL (site url)
|
// when sending login request to IdP we send relay state as URL (site url)
|
||||||
// with domainId as query parameter.
|
// with domainId or domainName as query parameter.
|
||||||
func (m *modelDao) GetDomainFromSsoResponse(ctx context.Context, relayState *url.URL) (*model.OrgDomain, error) {
|
func (m *modelDao) GetDomainFromSsoResponse(ctx context.Context, relayState *url.URL) (*model.OrgDomain, error) {
|
||||||
// derive domain id from relay state now
|
// derive domain id from relay state now
|
||||||
var domainIdStr string
|
var domainIdStr string
|
||||||
|
var domainNameStr string
|
||||||
|
var domain *model.OrgDomain
|
||||||
|
|
||||||
for k, v := range relayState.Query() {
|
for k, v := range relayState.Query() {
|
||||||
if k == "domainId" && len(v) > 0 {
|
if k == "domainId" && len(v) > 0 {
|
||||||
domainIdStr = strings.Replace(v[0], ":", "-", -1)
|
domainIdStr = strings.Replace(v[0], ":", "-", -1)
|
||||||
}
|
}
|
||||||
|
if k == "domainName" && len(v) > 0 {
|
||||||
|
domainNameStr = v[0]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
domainId, err := uuid.Parse(domainIdStr)
|
if domainIdStr != "" {
|
||||||
|
domainId, err := uuid.Parse(domainIdStr)
|
||||||
|
if err != nil {
|
||||||
|
zap.S().Errorf("failed to parse domainId from relay state", err)
|
||||||
|
return nil, fmt.Errorf("failed to parse domainId from IdP response")
|
||||||
|
}
|
||||||
|
|
||||||
|
domain, err = m.GetDomain(ctx, domainId)
|
||||||
|
if (err != nil) || domain == nil {
|
||||||
|
zap.S().Errorf("failed to find domain from domainId received in IdP response", err.Error())
|
||||||
|
return nil, fmt.Errorf("invalid credentials")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if domainNameStr != "" {
|
||||||
|
|
||||||
|
domainFromDB, err := m.GetDomainByName(ctx, domainNameStr)
|
||||||
|
domain = domainFromDB
|
||||||
|
if (err != nil) || domain == nil {
|
||||||
|
zap.S().Errorf("failed to find domain from domainName received in IdP response", err.Error())
|
||||||
|
return nil, fmt.Errorf("invalid credentials")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if domain != nil {
|
||||||
|
return domain, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("failed to find domain received in IdP response")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDomainByName returns org domain for a given domain name
|
||||||
|
func (m *modelDao) GetDomainByName(ctx context.Context, name string) (*model.OrgDomain, basemodel.BaseApiError) {
|
||||||
|
|
||||||
|
stored := StoredDomain{}
|
||||||
|
err := m.DB().Get(&stored, `SELECT * FROM org_domains WHERE name=$1 LIMIT 1`, name)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.S().Errorf("failed to parse domain id from relay state", err)
|
if err == sql.ErrNoRows {
|
||||||
return nil, fmt.Errorf("failed to parse response from IdP response")
|
return nil, model.BadRequest(fmt.Errorf("invalid domain name"))
|
||||||
|
}
|
||||||
|
return nil, model.InternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
domain, err := m.GetDomain(ctx, domainId)
|
domain := &model.OrgDomain{Id: stored.Id, Name: stored.Name, OrgId: stored.OrgId}
|
||||||
if (err != nil) || domain == nil {
|
if err := domain.LoadConfig(stored.Data); err != nil {
|
||||||
zap.S().Errorf("failed to find domain received in IdP response", err.Error())
|
return domain, model.InternalError(err)
|
||||||
return nil, fmt.Errorf("invalid credentials")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return domain, nil
|
return domain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -61,7 +62,6 @@ func InternalError(err error) *ApiError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// InternalErrorStr returns a ApiError object of internal type for string input
|
// InternalErrorStr returns a ApiError object of internal type for string input
|
||||||
func InternalErrorStr(s string) *ApiError {
|
func InternalErrorStr(s string) *ApiError {
|
||||||
return &ApiError{
|
return &ApiError{
|
||||||
@ -69,6 +69,7 @@ func InternalErrorStr(s string) *ApiError {
|
|||||||
Err: fmt.Errorf(s),
|
Err: fmt.Errorf(s),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrorNone basemodel.ErrorType = ""
|
ErrorNone basemodel.ErrorType = ""
|
||||||
ErrorTimeout basemodel.ErrorType = "timeout"
|
ErrorTimeout basemodel.ErrorType = "timeout"
|
||||||
|
@ -165,7 +165,7 @@ func ResetPassword(ctx context.Context, req *model.ResetPasswordRequest) error {
|
|||||||
return errors.New("Invalid reset password request")
|
return errors.New("Invalid reset password request")
|
||||||
}
|
}
|
||||||
|
|
||||||
hash, err := passwordHash(req.Password)
|
hash, err := PasswordHash(req.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Failed to generate password hash")
|
return errors.Wrap(err, "Failed to generate password hash")
|
||||||
}
|
}
|
||||||
@ -192,7 +192,7 @@ func ChangePassword(ctx context.Context, req *model.ChangePasswordRequest) error
|
|||||||
return ErrorInvalidCreds
|
return ErrorInvalidCreds
|
||||||
}
|
}
|
||||||
|
|
||||||
hash, err := passwordHash(req.NewPassword)
|
hash, err := PasswordHash(req.NewPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Failed to generate password hash")
|
return errors.Wrap(err, "Failed to generate password hash")
|
||||||
}
|
}
|
||||||
@ -243,7 +243,7 @@ func RegisterFirstUser(ctx context.Context, req *RegisterRequest) (*model.User,
|
|||||||
var hash string
|
var hash string
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
hash, err = passwordHash(req.Password)
|
hash, err = PasswordHash(req.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.S().Errorf("failed to generate password hash when registering a user", zap.Error(err))
|
zap.S().Errorf("failed to generate password hash when registering a user", zap.Error(err))
|
||||||
return nil, model.InternalError(model.ErrSignupFailed{})
|
return nil, model.InternalError(model.ErrSignupFailed{})
|
||||||
@ -314,13 +314,13 @@ func RegisterInvitedUser(ctx context.Context, req *RegisterRequest, nopassword b
|
|||||||
|
|
||||||
// check if password is not empty, as for SSO case it can be
|
// check if password is not empty, as for SSO case it can be
|
||||||
if req.Password != "" {
|
if req.Password != "" {
|
||||||
hash, err = passwordHash(req.Password)
|
hash, err = PasswordHash(req.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.S().Errorf("failed to generate password hash when registering a user", zap.Error(err))
|
zap.S().Errorf("failed to generate password hash when registering a user", zap.Error(err))
|
||||||
return nil, model.InternalError(model.ErrSignupFailed{})
|
return nil, model.InternalError(model.ErrSignupFailed{})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hash, err = passwordHash(utils.GeneratePassowrd())
|
hash, err = PasswordHash(utils.GeneratePassowrd())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.S().Errorf("failed to generate password hash when registering a user", zap.Error(err))
|
zap.S().Errorf("failed to generate password hash when registering a user", zap.Error(err))
|
||||||
return nil, model.InternalError(model.ErrSignupFailed{})
|
return nil, model.InternalError(model.ErrSignupFailed{})
|
||||||
@ -419,7 +419,7 @@ func authenticateLogin(ctx context.Context, req *model.LoginRequest) (*model.Use
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate hash from the password.
|
// Generate hash from the password.
|
||||||
func passwordHash(pass string) (string, error) {
|
func PasswordHash(pass string) (string, error) {
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
|
hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
Loading…
x
Reference in New Issue
Block a user