diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go
index 85bec52122..fd44fba29a 100644
--- a/ee/query-service/app/api/api.go
+++ b/ee/query-service/app/api/api.go
@@ -93,6 +93,10 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router) {
baseapp.OpenAccess(ah.receiveSAML)).
Methods(http.MethodPost)
+ router.HandleFunc("/api/v1/complete/google",
+ baseapp.OpenAccess(ah.receiveGoogleAuth)).
+ Methods(http.MethodGet)
+
router.HandleFunc("/api/v1/orgs/{orgId}/domains",
baseapp.AdminAccess(ah.listDomainsByOrg)).
Methods(http.MethodGet)
diff --git a/ee/query-service/app/api/auth.go b/ee/query-service/app/api/auth.go
index 0c99edfc36..2e622408be 100644
--- a/ee/query-service/app/api/auth.go
+++ b/ee/query-service/app/api/auth.go
@@ -8,9 +8,6 @@ import (
"io/ioutil"
"net/http"
"net/url"
- "strings"
-
- "github.com/google/uuid"
"github.com/gorilla/mux"
"go.signoz.io/signoz/ee/query-service/constants"
"go.signoz.io/signoz/ee/query-service/model"
@@ -184,114 +181,152 @@ func (ah *APIHandler) precheckLogin(w http.ResponseWriter, r *http.Request) {
ah.Respond(w, resp)
}
-func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
- // this is the source url that initiated the login request
+func handleSsoError(w http.ResponseWriter, r *http.Request, redirectURL string) {
+ ssoError := []byte("Login failed. Please contact your system administrator")
+ dst := make([]byte, base64.StdEncoding.EncodedLen(len(ssoError)))
+ base64.StdEncoding.Encode(dst, ssoError)
+
+ http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectURL, string(dst)), http.StatusSeeOther)
+}
+
+// receiveGoogleAuth completes google OAuth response and forwards a request
+// to front-end to sign user in
+func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request) {
redirectUri := constants.GetDefaultSiteURL()
ctx := context.Background()
- var apierr basemodel.BaseApiError
-
- redirectOnError := func() {
- ssoError := []byte("Login failed. Please contact your system administrator")
- dst := make([]byte, base64.StdEncoding.EncodedLen(len(ssoError)))
- base64.StdEncoding.Encode(dst, ssoError)
-
- http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, string(dst)), http.StatusMovedPermanently)
- }
-
if !ah.CheckFeature(model.SSO) {
- zap.S().Errorf("[ReceiveSAML] sso requested but feature unavailable %s in org domain %s", model.SSO)
+ zap.S().Errorf("[receiveGoogleAuth] sso requested but feature unavailable %s in org domain %s", model.SSO)
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
return
}
- err := r.ParseForm()
- if err != nil {
- zap.S().Errorf("[ReceiveSAML] failed to process response - invalid response from IDP", err, r)
- redirectOnError()
+ q := r.URL.Query()
+ if errType := q.Get("error"); errType != "" {
+ zap.S().Errorf("[receiveGoogleAuth] failed to login with google auth", q.Get("error_description"))
+ http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "failed to login through SSO "), http.StatusMovedPermanently)
return
}
- // the relay state is sent when a login request is submitted to
- // Idp.
- relayState := r.FormValue("RelayState")
- zap.S().Debug("[ReceiveML] relay state", zap.String("relayState", relayState))
+ relayState := q.Get("state")
+ zap.S().Debug("[receiveGoogleAuth] relay state received", zap.String("state", relayState))
parsedState, err := url.Parse(relayState)
if err != nil || relayState == "" {
- zap.S().Errorf("[ReceiveSAML] failed to process response - invalid response from IDP", err, r)
- redirectOnError()
+ zap.S().Errorf("[receiveGoogleAuth] failed to process response - invalid response from IDP", err, r)
+ handleSsoError(w, r, redirectUri)
return
}
// upgrade redirect url from the relay state for better accuracy
redirectUri = fmt.Sprintf("%s://%s%s", parsedState.Scheme, parsedState.Host, "/login")
- // derive domain id from relay state now
- var domainIdStr string
- for k, v := range parsedState.Query() {
- if k == "domainId" && len(v) > 0 {
- domainIdStr = strings.Replace(v[0], ":", "-", -1)
- }
- }
-
- domainId, err := uuid.Parse(domainIdStr)
+ // fetch domain by parsing relay state.
+ domain, err := ah.AppDao().GetDomainFromSsoResponse(ctx, parsedState)
if err != nil {
- zap.S().Errorf("[ReceiveSAML] failed to process request- failed to parse domain id ifrom relay", zap.Error(err))
- redirectOnError()
+ handleSsoError(w, r, redirectUri)
return
}
- domain, apierr := ah.AppDao().GetDomain(ctx, domainId)
- if (apierr != nil) || domain == nil {
- zap.S().Errorf("[ReceiveSAML] failed to process request- invalid domain", domainIdStr, zap.Error(apierr))
- redirectOnError()
+ // now that we have domain, use domain to fetch sso settings.
+ // prepare google callback handler using parsedState -
+ // which contains redirect URL (front-end endpoint)
+ callbackHandler, err := domain.PrepareGoogleOAuthProvider(parsedState)
+
+ identity, err := callbackHandler.HandleCallback(r)
+ if err != nil {
+ zap.S().Errorf("[receiveGoogleAuth] failed to process HandleCallback ", domain.String(), zap.Error(err))
+ handleSsoError(w, r, redirectUri)
+ return
+ }
+
+ nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, identity.Email)
+ if err != nil {
+ zap.S().Errorf("[receiveGoogleAuth] failed to generate redirect URI after successful login ", domain.String(), zap.Error(err))
+ handleSsoError(w, r, redirectUri)
return
}
+ http.Redirect(w, r, nextPage, http.StatusSeeOther)
+}
+
+
+
+// receiveSAML completes a SAML request and gets user logged in
+func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
+ // this is the source url that initiated the login request
+ redirectUri := constants.GetDefaultSiteURL()
+ ctx := context.Background()
+
+
+ if !ah.CheckFeature(model.SSO) {
+ zap.S().Errorf("[receiveSAML] sso requested but feature unavailable %s in org domain %s", model.SSO)
+ http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
+ return
+ }
+
+ err := r.ParseForm()
+ if err != nil {
+ zap.S().Errorf("[receiveSAML] failed to process response - invalid response from IDP", err, r)
+ handleSsoError(w, r, redirectUri)
+ return
+ }
+
+ // the relay state is sent when a login request is submitted to
+ // Idp.
+ relayState := r.FormValue("RelayState")
+ zap.S().Debug("[receiveML] relay state", zap.String("relayState", relayState))
+
+ parsedState, err := url.Parse(relayState)
+ if err != nil || relayState == "" {
+ zap.S().Errorf("[receiveSAML] failed to process response - invalid response from IDP", err, r)
+ handleSsoError(w, r, redirectUri)
+ return
+ }
+
+ // upgrade redirect url from the relay state for better accuracy
+ redirectUri = fmt.Sprintf("%s://%s%s", parsedState.Scheme, parsedState.Host, "/login")
+
+ // fetch domain by parsing relay state.
+ domain, err := ah.AppDao().GetDomainFromSsoResponse(ctx, parsedState)
+ if err != nil {
+ handleSsoError(w, r, redirectUri)
+ return
+ }
+
sp, err := domain.PrepareSamlRequest(parsedState)
if err != nil {
- zap.S().Errorf("[ReceiveSAML] failed to prepare saml request for domain (%s): %v", domainId, err)
- redirectOnError()
+ zap.S().Errorf("[receiveSAML] failed to prepare saml request for domain (%s): %v", domain.String(), err)
+ handleSsoError(w, r, redirectUri)
return
}
assertionInfo, err := sp.RetrieveAssertionInfo(r.FormValue("SAMLResponse"))
if err != nil {
- zap.S().Errorf("[ReceiveSAML] failed to retrieve assertion info from saml response for organization (%s): %v", domainId, err)
- redirectOnError()
+ zap.S().Errorf("[receiveSAML] failed to retrieve assertion info from saml response for organization (%s): %v", domain.String(), err)
+ handleSsoError(w, r, redirectUri)
return
}
if assertionInfo.WarningInfo.InvalidTime {
- zap.S().Errorf("[ReceiveSAML] expired saml response for organization (%s): %v", domainId, err)
- redirectOnError()
+ zap.S().Errorf("[receiveSAML] expired saml response for organization (%s): %v", domain.String(), err)
+ handleSsoError(w, r, redirectUri)
return
}
email := assertionInfo.NameID
-
- // user email found, now start preparing jwt response
- userPayload, baseapierr := ah.AppDao().GetUserByEmail(ctx, email)
- if baseapierr != nil {
- zap.S().Errorf("[ReceiveSAML] failed to find or register a new user for email %s and org %s", email, domainId, zap.Error(baseapierr.Err))
- redirectOnError()
+ if email == "" {
+ zap.S().Errorf("[receiveSAML] invalid email in the SSO response (%s)", domain.String())
+ handleSsoError(w, r, redirectUri)
return
}
- tokenStore, err := baseauth.GenerateJWTForUser(&userPayload.User)
+ nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, email)
if err != nil {
- zap.S().Errorf("[ReceiveSAML] failed to generate access token for email %s and org %s", email, domainId, zap.Error(err))
- redirectOnError()
+ zap.S().Errorf("[receiveSAML] failed to generate redirect URI after successful login ", domain.String(), zap.Error(err))
+ handleSsoError(w, r, redirectUri)
return
}
-
- userID := userPayload.User.Id
- nextPage := fmt.Sprintf("%s?jwt=%s&usr=%s&refreshjwt=%s",
- redirectUri,
- tokenStore.AccessJwt,
- userID,
- tokenStore.RefreshJwt)
-
- http.Redirect(w, r, nextPage, http.StatusMovedPermanently)
+
+ http.Redirect(w, r, nextPage, http.StatusSeeOther)
}
diff --git a/ee/query-service/dao/interface.go b/ee/query-service/dao/interface.go
index 7e17dcb635..a2c9d9d68d 100644
--- a/ee/query-service/dao/interface.go
+++ b/ee/query-service/dao/interface.go
@@ -2,7 +2,7 @@ package dao
import (
"context"
-
+ "net/url"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"go.signoz.io/signoz/ee/query-service/model"
@@ -22,7 +22,9 @@ type ModelDao interface {
// auth methods
PrecheckLogin(ctx context.Context, email, sourceUrl string) (*model.PrecheckResponse, basemodel.BaseApiError)
CanUsePassword(ctx context.Context, email string) (bool, basemodel.BaseApiError)
-
+ PrepareSsoRedirect(ctx context.Context, redirectUri, email string) (redirectURL string, apierr basemodel.BaseApiError)
+ GetDomainFromSsoResponse(ctx context.Context, relayState *url.URL) (*model.OrgDomain, error)
+
// org domain (auth domains) CRUD ops
ListDomains(ctx context.Context, orgId string) ([]model.OrgDomain, basemodel.BaseApiError)
GetDomain(ctx context.Context, id uuid.UUID) (*model.OrgDomain, basemodel.BaseApiError)
diff --git a/ee/query-service/dao/sqlite/auth.go b/ee/query-service/dao/sqlite/auth.go
index 13fd57259f..6eb46d2ea6 100644
--- a/ee/query-service/dao/sqlite/auth.go
+++ b/ee/query-service/dao/sqlite/auth.go
@@ -10,9 +10,33 @@ import (
"go.signoz.io/signoz/ee/query-service/model"
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
+ baseauth "go.signoz.io/signoz/pkg/query-service/auth"
"go.uber.org/zap"
)
+// PrepareSsoRedirect prepares redirect page link after SSO response
+// is successfully parsed (i.e. valid email is available)
+func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email string) (redirectURL string, apierr basemodel.BaseApiError) {
+
+ userPayload, apierr := m.GetUserByEmail(ctx, email)
+ if !apierr.IsNil() {
+ zap.S().Errorf(" failed to get user with email received from auth provider", apierr.Error())
+ return "", model.BadRequestStr("invalid user email received from the auth provider")
+ }
+
+ tokenStore, err := baseauth.GenerateJWTForUser(&userPayload.User)
+ if err != nil {
+ zap.S().Errorf("failed to generate token for SSO login user", err)
+ return "", model.InternalErrorStr("failed to generate token for the user")
+ }
+
+ return fmt.Sprintf("%s?jwt=%s&usr=%s&refreshjwt=%s",
+ redirectUri,
+ tokenStore.AccessJwt,
+ userPayload.User.Id,
+ tokenStore.RefreshJwt), nil
+}
+
func (m *modelDao) CanUsePassword(ctx context.Context, email string) (bool, basemodel.BaseApiError) {
domain, apierr := m.GetDomainByEmail(ctx, email)
if apierr != nil {
diff --git a/ee/query-service/dao/sqlite/domain.go b/ee/query-service/dao/sqlite/domain.go
index b98bc70cdb..d1ef8aa8d2 100644
--- a/ee/query-service/dao/sqlite/domain.go
+++ b/ee/query-service/dao/sqlite/domain.go
@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"encoding/json"
+ "net/url"
"fmt"
"strings"
"time"
@@ -25,6 +26,34 @@ type StoredDomain struct {
UpdatedAt int64 `db:"updated_at"`
}
+// GetDomainFromSsoResponse uses relay state received from IdP to fetch
+// 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)
+// with domainId as query parameter.
+func (m *modelDao) GetDomainFromSsoResponse(ctx context.Context, relayState *url.URL) (*model.OrgDomain, error) {
+ // derive domain id from relay state now
+ var domainIdStr string
+ for k, v := range relayState.Query() {
+ if k == "domainId" && len(v) > 0 {
+ domainIdStr = strings.Replace(v[0], ":", "-", -1)
+ }
+ }
+
+ domainId, err := uuid.Parse(domainIdStr)
+ if err != nil {
+ zap.S().Errorf("failed to parse domain id from relay state", err)
+ return nil, fmt.Errorf("failed to parse response from IdP response")
+ }
+
+ domain, err := m.GetDomain(ctx, domainId)
+ if (err != nil) || domain == nil {
+ zap.S().Errorf("failed to find domain received in IdP response", err.Error())
+ return nil, fmt.Errorf("invalid credentials")
+ }
+
+ return domain, nil
+}
+
// GetDomain returns org domain for a given domain id
func (m *modelDao) GetDomain(ctx context.Context, id uuid.UUID) (*model.OrgDomain, basemodel.BaseApiError) {
diff --git a/ee/query-service/model/domain.go b/ee/query-service/model/domain.go
index acde0e2194..beadd66a51 100644
--- a/ee/query-service/model/domain.go
+++ b/ee/query-service/model/domain.go
@@ -9,8 +9,10 @@ import (
"github.com/google/uuid"
"github.com/pkg/errors"
saml2 "github.com/russellhaering/gosaml2"
- "go.signoz.io/signoz/ee/query-service/saml"
+ "go.signoz.io/signoz/ee/query-service/sso/saml"
+ "go.signoz.io/signoz/ee/query-service/sso"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
+ "go.uber.org/zap"
)
type SSOType string
@@ -20,12 +22,6 @@ const (
GoogleAuth SSOType = "GOOGLE_AUTH"
)
-type SamlConfig struct {
- SamlEntity string `json:"samlEntity"`
- SamlIdp string `json:"samlIdp"`
- SamlCert string `json:"samlCert"`
-}
-
// OrgDomain identify org owned web domains for auth and other purposes
type OrgDomain struct {
Id uuid.UUID `json:"id"`
@@ -33,10 +29,17 @@ type OrgDomain struct {
OrgId string `json:"orgId"`
SsoEnabled bool `json:"ssoEnabled"`
SsoType SSOType `json:"ssoType"`
+
SamlConfig *SamlConfig `json:"samlConfig"`
+ GoogleAuthConfig *GoogleOAuthConfig `json:"googleAuthConfig"`
+
Org *basemodel.Organization
}
+func (od *OrgDomain) String() string {
+ return fmt.Sprintf("[%s]%s-%s ", od.Name, od.Id.String(), od.SsoType)
+}
+
// Valid is used a pipeline function to check if org domain
// loaded from db is valid
func (od *OrgDomain) Valid(err error) error {
@@ -97,6 +100,16 @@ func (od *OrgDomain) GetSAMLCert() string {
return ""
}
+// PrepareGoogleOAuthProvider creates GoogleProvider that is used in
+// requesting OAuth and also used in processing response from google
+func (od *OrgDomain) PrepareGoogleOAuthProvider(siteUrl *url.URL) (sso.OAuthCallbackProvider, error) {
+ if od.GoogleAuthConfig == nil {
+ return nil, fmt.Errorf("Google auth is not setup correctly for this domain")
+ }
+
+ return od.GoogleAuthConfig.GetProvider(od.Name, siteUrl)
+}
+
// PrepareSamlRequest creates a request accordingly gosaml2
func (od *OrgDomain) PrepareSamlRequest(siteUrl *url.URL) (*saml2.SAMLServiceProvider, error) {
@@ -124,19 +137,48 @@ func (od *OrgDomain) PrepareSamlRequest(siteUrl *url.URL) (*saml2.SAMLServicePro
}
func (od *OrgDomain) BuildSsoUrl(siteUrl *url.URL) (ssoUrl string, err error) {
-
- sp, err := od.PrepareSamlRequest(siteUrl)
- if err != nil {
- return "", err
- }
+
fmtDomainId := strings.Replace(od.Id.String(), "-", ":", -1)
+
+ // build redirect url from window.location sent by frontend
+ redirectURL := fmt.Sprintf("%s://%s%s", siteUrl.Scheme, siteUrl.Host, siteUrl.Path)
+
+ // prepare state that gets relayed back when the auth provider
+ // calls back our url. here we pass the app url (where signoz runs)
+ // and the domain Id. The domain Id helps in identifying sso config
+ // when the call back occurs and the app url is useful in redirecting user
+ // back to the right path.
+ // why do we need to pass app url? the callback typically is handled by backend
+ // and sometimes backend might right at a different port or is unaware of frontend
+ // endpoint (unless SITE_URL param is set). hence, we receive this build sso request
+ // along with frontend window.location and use it to relay the information through
+ // auth provider to the backend (HandleCallback or HandleSSO method).
+ relayState := fmt.Sprintf("%s?domainId=%s", redirectURL, fmtDomainId)
+
+
+ switch (od.SsoType) {
+ case SAML:
+
+ sp, err := od.PrepareSamlRequest(siteUrl)
+ if err != nil {
+ return "", err
+ }
+
+ return sp.BuildAuthURL(relayState)
+
+ case GoogleAuth:
+
+ googleProvider, err := od.PrepareGoogleOAuthProvider(siteUrl)
+ if err != nil {
+ return "", err
+ }
+ return googleProvider.BuildAuthURL(relayState)
+
+ default:
+ zap.S().Errorf("found unsupported SSO config for the org domain", zap.String("orgDomain", od.Name))
+ return "", fmt.Errorf("unsupported SSO config for the domain")
+ }
- relayState := fmt.Sprintf("%s://%s%s?domainId=%s",
- siteUrl.Scheme,
- siteUrl.Host,
- siteUrl.Path,
- fmtDomainId)
- return sp.BuildAuthURL(relayState)
}
diff --git a/ee/query-service/model/errors.go b/ee/query-service/model/errors.go
index 4c49f515c1..6820cf8d44 100644
--- a/ee/query-service/model/errors.go
+++ b/ee/query-service/model/errors.go
@@ -1,6 +1,7 @@
package model
import (
+ "fmt"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
)
@@ -44,6 +45,14 @@ func BadRequest(err error) *ApiError {
}
}
+// BadRequestStr returns a ApiError object of bad request for string input
+func BadRequestStr(s string) *ApiError {
+ return &ApiError{
+ Typ: basemodel.ErrorBadData,
+ Err: fmt.Errorf(s),
+ }
+}
+
// InternalError returns a ApiError object of internal type
func InternalError(err error) *ApiError {
return &ApiError{
@@ -52,6 +61,14 @@ func InternalError(err error) *ApiError {
}
}
+
+// InternalErrorStr returns a ApiError object of internal type for string input
+func InternalErrorStr(s string) *ApiError {
+ return &ApiError{
+ Typ: basemodel.ErrorInternal,
+ Err: fmt.Errorf(s),
+ }
+}
var (
ErrorNone basemodel.ErrorType = ""
ErrorTimeout basemodel.ErrorType = "timeout"
diff --git a/ee/query-service/model/sso.go b/ee/query-service/model/sso.go
new file mode 100644
index 0000000000..8e8e847433
--- /dev/null
+++ b/ee/query-service/model/sso.go
@@ -0,0 +1,68 @@
+package model
+
+import (
+ "fmt"
+ "context"
+ "net/url"
+ "golang.org/x/oauth2"
+ "github.com/coreos/go-oidc/v3/oidc"
+ "go.signoz.io/signoz/ee/query-service/sso"
+)
+
+// SamlConfig contans SAML params to generate and respond to the requests
+// from SAML provider
+type SamlConfig struct {
+ SamlEntity string `json:"samlEntity"`
+ SamlIdp string `json:"samlIdp"`
+ SamlCert string `json:"samlCert"`
+}
+
+// GoogleOauthConfig contains a generic config to support oauth
+type GoogleOAuthConfig struct {
+ ClientID string `json:"clientId"`
+ ClientSecret string `json:"clientSecret"`
+ RedirectURI string `json:"redirectURI"`
+}
+
+
+const (
+ googleIssuerURL = "https://accounts.google.com"
+)
+
+func (g *GoogleOAuthConfig) GetProvider(domain string, siteUrl *url.URL) (sso.OAuthCallbackProvider, error) {
+
+ ctx, cancel := context.WithCancel(context.Background())
+
+ provider, err := oidc.NewProvider(ctx, googleIssuerURL)
+ if err != nil {
+ cancel()
+ return nil, fmt.Errorf("failed to get provider: %v", err)
+ }
+
+ // default to email and profile scope as we just use google auth
+ // to verify identity and start a session.
+ scopes := []string{"email"}
+
+ // this is the url google will call after login completion
+ redirectURL := fmt.Sprintf("%s://%s/%s",
+ siteUrl.Scheme,
+ siteUrl.Host,
+ "api/v1/complete/google")
+
+ return &sso.GoogleOAuthProvider{
+ RedirectURI: g.RedirectURI,
+ OAuth2Config: &oauth2.Config{
+ ClientID: g.ClientID,
+ ClientSecret: g.ClientSecret,
+ Endpoint: provider.Endpoint(),
+ Scopes: scopes,
+ RedirectURL: redirectURL,
+ },
+ Verifier: provider.Verifier(
+ &oidc.Config{ClientID: g.ClientID},
+ ),
+ Cancel: cancel,
+ HostedDomain: domain,
+ }, nil
+}
+
diff --git a/ee/query-service/sso/google.go b/ee/query-service/sso/google.go
new file mode 100644
index 0000000000..a27a38eb1e
--- /dev/null
+++ b/ee/query-service/sso/google.go
@@ -0,0 +1,92 @@
+package sso
+
+import (
+ "fmt"
+ "errors"
+ "context"
+ "net/http"
+ "github.com/coreos/go-oidc/v3/oidc"
+ "golang.org/x/oauth2"
+)
+
+type GoogleOAuthProvider struct {
+ RedirectURI string
+ OAuth2Config *oauth2.Config
+ Verifier *oidc.IDTokenVerifier
+ Cancel context.CancelFunc
+ HostedDomain string
+}
+
+
+func (g *GoogleOAuthProvider) BuildAuthURL(state string) (string, error) {
+ var opts []oauth2.AuthCodeOption
+
+ // set hosted domain. google supports multiple hosted domains but in our case
+ // we have one config per host domain.
+ opts = append(opts, oauth2.SetAuthURLParam("hd", g.HostedDomain))
+
+ return g.OAuth2Config.AuthCodeURL(state, opts...), nil
+}
+
+type oauth2Error struct{
+ error string
+ errorDescription string
+}
+
+func (e *oauth2Error) Error() string {
+ if e.errorDescription == "" {
+ return e.error
+ }
+ return e.error + ": " + e.errorDescription
+}
+
+func (g *GoogleOAuthProvider) HandleCallback(r *http.Request) (identity *SSOIdentity, err error) {
+ q := r.URL.Query()
+ if errType := q.Get("error"); errType != "" {
+ return identity, &oauth2Error{errType, q.Get("error_description")}
+ }
+
+ token, err := g.OAuth2Config.Exchange(r.Context(), q.Get("code"))
+ if err != nil {
+ return identity, fmt.Errorf("google: failed to get token: %v", err)
+ }
+
+ return g.createIdentity(r.Context(), token)
+}
+
+
+func (g *GoogleOAuthProvider) createIdentity(ctx context.Context, token *oauth2.Token) (identity *SSOIdentity, err error) {
+ rawIDToken, ok := token.Extra("id_token").(string)
+ if !ok {
+ return identity, errors.New("google: no id_token in token response")
+ }
+ idToken, err := g.Verifier.Verify(ctx, rawIDToken)
+ if err != nil {
+ return identity, fmt.Errorf("google: failed to verify ID Token: %v", err)
+ }
+
+ var claims struct {
+ Username string `json:"name"`
+ Email string `json:"email"`
+ EmailVerified bool `json:"email_verified"`
+ HostedDomain string `json:"hd"`
+ }
+ if err := idToken.Claims(&claims); err != nil {
+ return identity, fmt.Errorf("oidc: failed to decode claims: %v", err)
+ }
+
+ if claims.HostedDomain != g.HostedDomain {
+ return identity, fmt.Errorf("oidc: unexpected hd claim %v", claims.HostedDomain)
+ }
+
+ identity = &SSOIdentity{
+ UserID: idToken.Subject,
+ Username: claims.Username,
+ Email: claims.Email,
+ EmailVerified: claims.EmailVerified,
+ ConnectorData: []byte(token.RefreshToken),
+ }
+
+ return identity, nil
+}
+
diff --git a/ee/query-service/sso/model.go b/ee/query-service/sso/model.go
new file mode 100644
index 0000000000..3e5f103b75
--- /dev/null
+++ b/ee/query-service/sso/model.go
@@ -0,0 +1,31 @@
+package sso
+
+import (
+ "net/http"
+)
+
+// SSOIdentity contains details of user received from SSO provider
+type SSOIdentity struct {
+ UserID string
+ Username string
+ PreferredUsername string
+ Email string
+ EmailVerified bool
+ ConnectorData []byte
+}
+
+// OAuthCallbackProvider is an interface implemented by connectors which use an OAuth
+// style redirect flow to determine user information.
+type OAuthCallbackProvider interface {
+ // The initial URL user would be redirect to.
+ // OAuth2 implementations support various scopes but we only need profile and user as
+ // the roles are still being managed in SigNoz.
+ BuildAuthURL(state string) (string, error)
+
+ // Handle the callback to the server (after login at oauth provider site)
+ // and return a email identity.
+ // At the moment we dont support auto signup flow (based on domain), so
+ // the full identity (including name, group etc) is not required outside of the
+ // connector
+ HandleCallback(r *http.Request) (identity *SSOIdentity, err error)
+}
diff --git a/ee/query-service/saml/request.go b/ee/query-service/sso/saml/request.go
similarity index 100%
rename from ee/query-service/saml/request.go
rename to ee/query-service/sso/saml/request.go
diff --git a/frontend/src/container/OrganizationSettings/AuthDomains/Create/Row/index.tsx b/frontend/src/container/OrganizationSettings/AuthDomains/Create/Row/index.tsx
index 2cf671f05f..96c5d0a2ef 100644
--- a/frontend/src/container/OrganizationSettings/AuthDomains/Create/Row/index.tsx
+++ b/frontend/src/container/OrganizationSettings/AuthDomains/Create/Row/index.tsx
@@ -1,7 +1,7 @@
import { Button, Space, Typography } from 'antd';
import React from 'react';
-import { IconContainer, TitleContainer } from './styles';
+import { IconContainer, TitleContainer, TitleText } from './styles';
function Row({
onClickHandler,
@@ -16,8 +16,8 @@ function Row({
{Icon}
- {title}
- {subTitle}
+ {title}
+ {subTitle}