Skip to content

Commit c122bc0

Browse files
feat(tokenizer|sso): add tokenizer for session management and oidc sso support (#9183)
## 📄 Summary - Instead of relying on JWT for session management, we are adding another token system: opaque. This gives the benefits of expiration and revocation. - We are now ensuring that emails are regex checked throughout the backend. - Support has been added for OIDC protocol
1 parent d22039b commit c122bc0

File tree

225 files changed

+9247
-9459
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

225 files changed

+9247
-9459
lines changed

.github/workflows/integrationci.yaml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ jobs:
1515
matrix:
1616
src:
1717
- bootstrap
18-
- auth
18+
- passwordauthn
19+
- callbackauthn
1920
- querier
2021
- ttl
2122
sqlstore-provider:
@@ -43,6 +44,20 @@ jobs:
4344
python -m pip install poetry==2.1.2
4445
python -m poetry config virtualenvs.in-project true
4546
cd tests/integration && poetry install --no-root
47+
- name: webdriver
48+
run: |
49+
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
50+
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee -a /etc/apt/sources.list.d/google-chrome.list
51+
sudo apt-get update -qqy
52+
sudo apt-get -qqy install google-chrome-stable
53+
CHROME_VERSION=$(google-chrome-stable --version)
54+
CHROME_FULL_VERSION=${CHROME_VERSION%%.*}
55+
CHROME_MAJOR_VERSION=${CHROME_FULL_VERSION//[!0-9]}
56+
sudo rm /etc/apt/sources.list.d/google-chrome.list
57+
export CHROMEDRIVER_VERSION=`curl -s https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_${CHROME_MAJOR_VERSION%%.*}`
58+
curl -L -O "https://storage.googleapis.com/chrome-for-testing-public/${CHROMEDRIVER_VERSION}/linux64/chromedriver-linux64.zip"
59+
unzip chromedriver-linux64.zip && chmod +x chromedriver && sudo mv chromedriver /usr/local/bin
60+
chromedriver -version
4661
- name: run
4762
run: |
4863
cd tests/integration && \

cmd/community/server.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ package main
33
import (
44
"context"
55
"log/slog"
6-
"time"
76

87
"github.com/SigNoz/signoz/cmd"
98
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
109
"github.com/SigNoz/signoz/pkg/analytics"
10+
"github.com/SigNoz/signoz/pkg/authn"
1111
"github.com/SigNoz/signoz/pkg/factory"
1212
"github.com/SigNoz/signoz/pkg/licensing"
1313
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
@@ -56,12 +56,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
5656
return err
5757
}
5858

59-
jwt := authtypes.NewJWT(cmd.NewJWTSecret(ctx, logger), 30*time.Minute, 30*24*time.Hour)
60-
6159
signoz, err := signoz.New(
6260
ctx,
6361
config,
64-
jwt,
6562
zeus.Config{},
6663
noopzeus.NewProviderFactory(),
6764
licensing.Config{},
@@ -76,13 +73,16 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
7673
},
7774
signoz.NewSQLStoreProviderFactories(),
7875
signoz.NewTelemetryStoreProviderFactories(),
76+
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
77+
return signoz.NewAuthNs(ctx, providerSettings, store, licensing)
78+
},
7979
)
8080
if err != nil {
8181
logger.ErrorContext(ctx, "failed to create signoz", "error", err)
8282
return err
8383
}
8484

85-
server, err := app.NewServer(config, signoz, jwt)
85+
server, err := app.NewServer(config, signoz)
8686
if err != nil {
8787
logger.ErrorContext(ctx, "failed to create server", "error", err)
8888
return err

cmd/config.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package cmd
33
import (
44
"context"
55
"log/slog"
6-
"os"
76

87
"github.com/SigNoz/signoz/pkg/config"
98
"github.com/SigNoz/signoz/pkg/config/envprovider"
@@ -30,12 +29,3 @@ func NewSigNozConfig(ctx context.Context, logger *slog.Logger, flags signoz.Depr
3029

3130
return config, nil
3231
}
33-
34-
func NewJWTSecret(ctx context.Context, logger *slog.Logger) string {
35-
jwtSecret := os.Getenv("SIGNOZ_JWT_SECRET")
36-
if len(jwtSecret) == 0 {
37-
logger.ErrorContext(ctx, "🚨 CRITICAL SECURITY ISSUE: No JWT secret key specified!", "error", "SIGNOZ_JWT_SECRET environment variable is not set. This has dire consequences for the security of the application. Without a JWT secret, user sessions are vulnerable to tampering and unauthorized access. Please set the SIGNOZ_JWT_SECRET environment variable immediately. For more information, please refer to https://github.com/SigNoz/signoz/issues/8400.")
38-
}
39-
40-
return jwtSecret
41-
}

cmd/enterprise/server.go

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"time"
77

88
"github.com/SigNoz/signoz/cmd"
9+
"github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn"
10+
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
911
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
1012
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
1113
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
@@ -14,6 +16,7 @@ import (
1416
enterprisezeus "github.com/SigNoz/signoz/ee/zeus"
1517
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
1618
"github.com/SigNoz/signoz/pkg/analytics"
19+
"github.com/SigNoz/signoz/pkg/authn"
1720
"github.com/SigNoz/signoz/pkg/factory"
1821
"github.com/SigNoz/signoz/pkg/licensing"
1922
"github.com/SigNoz/signoz/pkg/modules/organization"
@@ -54,17 +57,14 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
5457

5558
// add enterprise sqlstore factories to the community sqlstore factories
5659
sqlstoreFactories := signoz.NewSQLStoreProviderFactories()
57-
if err := sqlstoreFactories.Add(postgressqlstore.NewFactory(sqlstorehook.NewLoggingFactory())); err != nil {
60+
if err := sqlstoreFactories.Add(postgressqlstore.NewFactory(sqlstorehook.NewLoggingFactory(), sqlstorehook.NewInstrumentationFactory())); err != nil {
5861
logger.ErrorContext(ctx, "failed to add postgressqlstore factory", "error", err)
5962
return err
6063
}
6164

62-
jwt := authtypes.NewJWT(cmd.NewJWTSecret(ctx, logger), 30*time.Minute, 30*24*time.Hour)
63-
6465
signoz, err := signoz.New(
6566
ctx,
6667
config,
67-
jwt,
6868
enterprisezeus.Config(),
6969
httpzeus.NewProviderFactory(),
7070
enterpriselicensing.Config(24*time.Hour, 3),
@@ -84,13 +84,34 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
8484
},
8585
sqlstoreFactories,
8686
signoz.NewTelemetryStoreProviderFactories(),
87+
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
88+
samlCallbackAuthN, err := samlcallbackauthn.New(ctx, store, licensing)
89+
if err != nil {
90+
return nil, err
91+
}
92+
93+
oidcCallbackAuthN, err := oidccallbackauthn.New(store, licensing, providerSettings)
94+
if err != nil {
95+
return nil, err
96+
}
97+
98+
authNs, err := signoz.NewAuthNs(ctx, providerSettings, store, licensing)
99+
if err != nil {
100+
return nil, err
101+
}
102+
103+
authNs[authtypes.AuthNProviderSAML] = samlCallbackAuthN
104+
authNs[authtypes.AuthNProviderOIDC] = oidcCallbackAuthN
105+
106+
return authNs, nil
107+
},
87108
)
88109
if err != nil {
89110
logger.ErrorContext(ctx, "failed to create signoz", "error", err)
90111
return err
91112
}
92113

93-
server, err := enterpriseapp.NewServer(config, signoz, jwt)
114+
server, err := enterpriseapp.NewServer(config, signoz)
94115
if err != nil {
95116
logger.ErrorContext(ctx, "failed to create server", "error", err)
96117
return err

conf/example.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,3 +243,28 @@ statsreporter:
243243
gateway:
244244
# The URL of the gateway's api.
245245
url: http://localhost:8080
246+
247+
##################### Tokenizer #####################
248+
tokenizer:
249+
# Specifies the tokenizer provider to use.
250+
provider: jwt
251+
lifetime:
252+
# The duration for which a user can be idle before being required to authenticate.
253+
idle: 168h
254+
# The duration for which a user can remain logged in before being asked to login.
255+
max: 720h
256+
rotation:
257+
# The interval to rotate tokens in.
258+
interval: 30m
259+
# The duration for which the previous token pair remains valid after a token pair is rotated.
260+
duration: 60s
261+
jwt:
262+
# The secret to sign the JWT tokens.
263+
secret: secret
264+
opaque:
265+
gc:
266+
# The interval to perform garbage collection.
267+
interval: 1h
268+
token:
269+
# The maximum number of tokens a user can have. This limits the number of concurrent sessions a user can have.
270+
max_per_user: 5
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package oidccallbackauthn
2+
3+
import (
4+
"context"
5+
"net/url"
6+
7+
"github.com/SigNoz/signoz/pkg/authn"
8+
"github.com/SigNoz/signoz/pkg/errors"
9+
"github.com/SigNoz/signoz/pkg/factory"
10+
"github.com/SigNoz/signoz/pkg/http/client"
11+
"github.com/SigNoz/signoz/pkg/licensing"
12+
"github.com/SigNoz/signoz/pkg/types/authtypes"
13+
"github.com/SigNoz/signoz/pkg/valuer"
14+
"github.com/coreos/go-oidc/v3/oidc"
15+
"golang.org/x/oauth2"
16+
)
17+
18+
const (
19+
redirectPath string = "/api/v1/complete/oidc"
20+
)
21+
22+
var (
23+
scopes []string = []string{"email", oidc.ScopeOpenID}
24+
)
25+
26+
var _ authn.CallbackAuthN = (*AuthN)(nil)
27+
28+
type AuthN struct {
29+
store authtypes.AuthNStore
30+
licensing licensing.Licensing
31+
httpClient *client.Client
32+
}
33+
34+
func New(store authtypes.AuthNStore, licensing licensing.Licensing, providerSettings factory.ProviderSettings) (*AuthN, error) {
35+
httpClient, err := client.New(providerSettings.Logger, providerSettings.TracerProvider, providerSettings.MeterProvider)
36+
if err != nil {
37+
return nil, err
38+
}
39+
40+
return &AuthN{
41+
store: store,
42+
licensing: licensing,
43+
httpClient: httpClient,
44+
}, nil
45+
}
46+
47+
func (a *AuthN) LoginURL(ctx context.Context, siteURL *url.URL, authDomain *authtypes.AuthDomain) (string, error) {
48+
if authDomain.AuthDomainConfig().AuthNProvider != authtypes.AuthNProviderOIDC {
49+
return "", errors.Newf(errors.TypeInternal, authtypes.ErrCodeAuthDomainMismatch, "domain type is not oidc")
50+
}
51+
52+
_, oauth2Config, err := a.oidcProviderAndoauth2Config(ctx, siteURL, authDomain)
53+
if err != nil {
54+
return "", err
55+
}
56+
57+
return oauth2Config.AuthCodeURL(authtypes.NewState(siteURL, authDomain.StorableAuthDomain().ID).URL.String()), nil
58+
}
59+
60+
func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtypes.CallbackIdentity, error) {
61+
if err := query.Get("error"); err != "" {
62+
return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "oidc: error while authenticating").WithAdditional(query.Get("error_description"))
63+
}
64+
65+
state, err := authtypes.NewStateFromString(query.Get("state"))
66+
if err != nil {
67+
return nil, errors.Newf(errors.TypeInvalidInput, authtypes.ErrCodeInvalidState, "oidc: invalid state").WithAdditional(err.Error())
68+
}
69+
70+
authDomain, err := a.store.GetAuthDomainFromID(ctx, state.DomainID)
71+
if err != nil {
72+
return nil, err
73+
}
74+
75+
_, err = a.licensing.GetActive(ctx, authDomain.StorableAuthDomain().OrgID)
76+
if err != nil {
77+
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
78+
}
79+
80+
oidcProvider, oauth2Config, err := a.oidcProviderAndoauth2Config(ctx, state.URL, authDomain)
81+
if err != nil {
82+
return nil, err
83+
}
84+
85+
ctx = context.WithValue(ctx, oauth2.HTTPClient, a.httpClient.Client())
86+
token, err := oauth2Config.Exchange(ctx, query.Get("code"))
87+
if err != nil {
88+
var retrieveError *oauth2.RetrieveError
89+
if errors.As(err, &retrieveError) {
90+
return nil, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "oidc: failed to get token").WithAdditional(retrieveError.ErrorDescription).WithAdditional(string(retrieveError.Body))
91+
}
92+
93+
return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "oidc: failed to get token").WithAdditional(err.Error())
94+
}
95+
96+
claims, err := a.claimsFromIDToken(ctx, authDomain, oidcProvider, token)
97+
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
98+
return nil, err
99+
}
100+
101+
if claims == nil && authDomain.AuthDomainConfig().OIDC.GetUserInfo {
102+
claims, err = a.claimsFromUserInfo(ctx, oidcProvider, token)
103+
if err != nil {
104+
return nil, err
105+
}
106+
}
107+
108+
emailClaim, ok := claims[authDomain.AuthDomainConfig().OIDC.ClaimMapping.Email].(string)
109+
if !ok {
110+
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "oidc: missing email in claims")
111+
}
112+
113+
email, err := valuer.NewEmail(emailClaim)
114+
if err != nil {
115+
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "oidc: failed to parse email").WithAdditional(err.Error())
116+
}
117+
118+
if !authDomain.AuthDomainConfig().OIDC.InsecureSkipEmailVerified {
119+
emailVerifiedClaim, ok := claims["email_verified"].(bool)
120+
if !ok {
121+
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "oidc: missing email_verified in claims")
122+
}
123+
124+
if !emailVerifiedClaim {
125+
return nil, errors.New(errors.TypeForbidden, errors.CodeForbidden, "oidc: email is not verified")
126+
}
127+
}
128+
129+
return authtypes.NewCallbackIdentity("", email, authDomain.StorableAuthDomain().OrgID, state), nil
130+
}
131+
132+
func (a *AuthN) oidcProviderAndoauth2Config(ctx context.Context, siteURL *url.URL, authDomain *authtypes.AuthDomain) (*oidc.Provider, *oauth2.Config, error) {
133+
if authDomain.AuthDomainConfig().OIDC.IssuerAlias != "" {
134+
ctx = oidc.InsecureIssuerURLContext(ctx, authDomain.AuthDomainConfig().OIDC.IssuerAlias)
135+
}
136+
137+
oidcProvider, err := oidc.NewProvider(ctx, authDomain.AuthDomainConfig().OIDC.Issuer)
138+
if err != nil {
139+
return nil, nil, err
140+
}
141+
142+
return oidcProvider, &oauth2.Config{
143+
ClientID: authDomain.AuthDomainConfig().OIDC.ClientID,
144+
ClientSecret: authDomain.AuthDomainConfig().OIDC.ClientSecret,
145+
Endpoint: oidcProvider.Endpoint(),
146+
Scopes: scopes,
147+
RedirectURL: (&url.URL{
148+
Scheme: siteURL.Scheme,
149+
Host: siteURL.Host,
150+
Path: redirectPath,
151+
}).String(),
152+
}, nil
153+
}
154+
155+
func (a *AuthN) claimsFromIDToken(ctx context.Context, authDomain *authtypes.AuthDomain, provider *oidc.Provider, token *oauth2.Token) (map[string]any, error) {
156+
rawIDToken, ok := token.Extra("id_token").(string)
157+
if !ok {
158+
return nil, errors.New(errors.TypeNotFound, errors.CodeNotFound, "oidc: no id_token in token response")
159+
}
160+
161+
verifier := provider.Verifier(&oidc.Config{ClientID: authDomain.AuthDomainConfig().OIDC.ClientID})
162+
idToken, err := verifier.Verify(ctx, rawIDToken)
163+
if err != nil {
164+
return nil, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "oidc: failed to verify token").WithAdditional(err.Error())
165+
}
166+
167+
var claims map[string]any
168+
if err := idToken.Claims(&claims); err != nil {
169+
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "oidc: failed to decode claims").WithAdditional(err.Error())
170+
}
171+
172+
return claims, nil
173+
}
174+
175+
func (a *AuthN) claimsFromUserInfo(ctx context.Context, provider *oidc.Provider, token *oauth2.Token) (map[string]any, error) {
176+
var claims map[string]any
177+
178+
userInfo, err := provider.UserInfo(ctx, oauth2.StaticTokenSource(&oauth2.Token{
179+
AccessToken: token.AccessToken,
180+
TokenType: "Bearer", // The UserInfo endpoint requires a bearer token as per RFC6750
181+
}))
182+
if err != nil {
183+
return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "oidc: failed to get user info").WithAdditional(err.Error())
184+
}
185+
186+
if err := userInfo.Claims(&claims); err != nil {
187+
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "oidc: failed to decode claims").WithAdditional(err.Error())
188+
}
189+
190+
return claims, nil
191+
}

0 commit comments

Comments
 (0)