mirror of
https://github.com/grafana/grafana.git
synced 2024-12-28 01:41:24 -06:00
Auth: Implement Token URL JWT Auth (#52662)
* Auth: check of auth_token in url and resolve user if present * check if auth_token is passed in url * Auth: Pass auth_token for request if present in path * no need to decode token in index * temp * use loadURLToken and set authorization header * cache token in memory and strip it from url * Use loadURLToken * Keep token in url * strip sensitive query strings from url used by context logger * adapt login by url to jwt token * add jwt iframe devenv * add jwt iframe devenv instructions * add access note * add test for cleaning request * ensure jwt token is not carried into handlers * do not reshuffle queries, might be important * add correct db dump location * prefer set token instead of cached token Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> Co-authored-by: Karl Persson <kalle.persson@grafana.com> Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>
This commit is contained in:
parent
7ba076de10
commit
c2d3c90bc8
@ -1688,6 +1688,7 @@ a5a8fed6-0bca-4646-9946-2fe84175353b t f account 0 f d0b8b6b6-2a02-412c-84d1-716
|
||||
805aebc8-9d01-42b6-bcce-6ce48ca63ef0 t f security-admin-console 0 t 27d2217e-9934-4971-93b8-77969e47ecf7 /admin/grafana/console/ f \N f grafana openid-connect 0 f f ${client_security-admin-console} f client-secret ${authAdminUrl} \N \N t f f f
|
||||
6bd2d943-9800-4839-9ddc-03c04930cd9f t f admin-cli 0 t da0811c3-5031-4f35-9dc5-441050461a37 \N f \N f grafana openid-connect 0 f f ${client_admin-cli} f client-secret \N \N \N f f t f
|
||||
09b79548-8426-4c0e-8e0b-7488467532c7 t t grafana-oauth 0 f d17b9ea9-bcb1-43d2-b132-d339e55872a8 http://127.0.0.1:8087 f http://127.0.0.1:8087 f grafana openid-connect -1 f f \N f client-secret http://127.0.0.1:8087 \N \N t f t f
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c t t sample-iframe-project 0 t c2ada58a-760e-40d7-8ddc-9ea69b465af2 \N f http://localhost:4200 f grafana openid-connect -1 f f \N f client-secret http://localhost:4200 \N \N t f t f
|
||||
\.
|
||||
|
||||
|
||||
@ -1717,6 +1718,22 @@ COPY public.client_attributes (client_id, value, name) FROM stdin;
|
||||
09b79548-8426-4c0e-8e0b-7488467532c7 false client_credentials.use_refresh_token
|
||||
09b79548-8426-4c0e-8e0b-7488467532c7 false display.on.consent.screen
|
||||
09b79548-8426-4c0e-8e0b-7488467532c7 backchannel.logout.url
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c true backchannel.logout.session.required
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c false backchannel.logout.revoke.offline.tokens
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c false saml.server.signature
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c false saml.server.signature.keyinfo.ext
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c false saml.assertion.signature
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c false saml.client.signature
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c false saml.encrypt
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c false saml.authnstatement
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c false saml.onetimeuse.condition
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c false saml_force_name_id_format
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c false saml.multivalued.roles
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c false saml.force.post.binding
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c false exclude.session.state.from.auth.response
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c false tls.client.certificate.bound.access.tokens
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c false client_credentials.use_refresh_token
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c false display.on.consent.screen
|
||||
\.
|
||||
|
||||
|
||||
@ -1975,6 +1992,14 @@ a8698f4f-5fa1-4baa-be05-87d03052af49 c61f5b19-c17e-49a1-91b8-a0296411b928 f
|
||||
09b79548-8426-4c0e-8e0b-7488467532c7 d4723cd4-f717-44b7-a9b0-6c32c5ecd23f t
|
||||
09b79548-8426-4c0e-8e0b-7488467532c7 0a7c7dde-23d7-4a93-bdee-4a8963aee9a4 t
|
||||
09b79548-8426-4c0e-8e0b-7488467532c7 74daf2cd-40d4-4304-87a8-92cdca808512 t
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c d6077ed7-b265-4f82-9336-24614967bd5d t
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c 74daf2cd-40d4-4304-87a8-92cdca808512 t
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c 96d521d3-facc-4b5a-a8b4-a879bae6be07 t
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c 699671ab-e7c1-4fcf-beb8-ea54f1471fc1 t
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c 0e98d5f9-d3f7-4b1d-9791-d442524fc2ab f
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c a5bb3a5f-fd26-4be6-9557-26e20a03d33d f
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c d6ffe9fc-a03c-4496-85dc-dbb5e7754587 f
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c c61f5b19-c17e-49a1-91b8-a0296411b928 f
|
||||
\.
|
||||
|
||||
|
||||
@ -3135,6 +3160,7 @@ a5a8fed6-0bca-4646-9946-2fe84175353b /realms/grafana/account/*
|
||||
230081b5-9161-45c3-9e08-9eda5412f7f7 /realms/grafana/account/*
|
||||
805aebc8-9d01-42b6-bcce-6ce48ca63ef0 /admin/grafana/console/*
|
||||
09b79548-8426-4c0e-8e0b-7488467532c7 http://127.0.0.1:8088/oauth2/callback
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c http://localhost:4200/*
|
||||
\.
|
||||
|
||||
|
||||
@ -3410,6 +3436,7 @@ COPY public.web_origins (client_id, value) FROM stdin;
|
||||
2f521d09-7304-4b5e-a94b-7cc7300b8b50 +
|
||||
805aebc8-9d01-42b6-bcce-6ce48ca63ef0 +
|
||||
09b79548-8426-4c0e-8e0b-7488467532c7 http://127.0.0.1:8087
|
||||
169f1dea-80f0-4a99-8509-9abb70ab0a5c http://localhost:4200
|
||||
\.
|
||||
|
||||
|
||||
|
@ -31,13 +31,34 @@ Access Grafana through:
|
||||
http://127.0.0.1:8088
|
||||
```
|
||||
|
||||
## Devenv setup jwt auth iframe embedding
|
||||
|
||||
- Add previous configuration and next snippet to grafana.ini
|
||||
|
||||
```ini
|
||||
[security]
|
||||
allow_embedding = true
|
||||
```
|
||||
|
||||
- Create dashboard and copy UID
|
||||
|
||||
- Clone [https://github.com/grafana/grafana-iframe-oauth-sample](https://github.com/grafana/grafana-iframe-oauth-sample)
|
||||
|
||||
- Change the dashboard URL in `grafana-iframe-oauth-sample/src/pages/restricted.tsx` to use the dashboard you created (keep URL query values)
|
||||
|
||||
- Start sample app from the `grafana-iframe-oauth-sample` folder with: `yarn start`
|
||||
|
||||
- Navigate to [http://localhost:4200](http://localhost:4200) and press restricted area
|
||||
|
||||
Note: You may need to grant the JWT user in grafana access to the datasources and the dashboard
|
||||
|
||||
## Backing up keycloak DB
|
||||
|
||||
In case you want to make changes to the devenv setup, you can dump keycloack's DB:
|
||||
|
||||
```bash
|
||||
cd devenv;
|
||||
docker-compose exec -T oauthkeycloakdb bash -c "pg_dump -U keycloak keycloak" > docker/blocks/oauth/cloak.sql
|
||||
docker-compose exec -T oauthkeycloakdb bash -c "pg_dump -U keycloak keycloak" > docker/blocks/jwt_proxy/cloak.sql
|
||||
```
|
||||
|
||||
## Connecting to keycloack:
|
||||
|
@ -52,6 +52,8 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
helpEnabled = false;
|
||||
profileEnabled = false;
|
||||
ldapEnabled = false;
|
||||
jwtHeaderName = '';
|
||||
jwtUrlLogin = false;
|
||||
sigV4AuthEnabled = false;
|
||||
samlEnabled = false;
|
||||
samlName = '';
|
||||
|
@ -98,6 +98,8 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
||||
"allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
|
||||
"authProxyEnabled": setting.AuthProxyEnabled,
|
||||
"ldapEnabled": hs.Cfg.LDAPEnabled,
|
||||
"jwtHeaderName": hs.Cfg.JWTAuthHeaderName,
|
||||
"jwtUrlLogin": hs.Cfg.JWTAuthURLLogin,
|
||||
"alertingEnabled": setting.AlertingEnabled,
|
||||
"alertingErrorOrTimeout": setting.AlertingErrorOrTimeout,
|
||||
"alertingNoDataOrNullValues": setting.AlertingNoDataOrNullValues,
|
||||
|
@ -57,7 +57,7 @@ func Logger(cfg *setting.Cfg) web.Handler {
|
||||
"time_ms", int64(timeTaken),
|
||||
"duration", duration,
|
||||
"size", rw.Size(),
|
||||
"referer", sanitizeURL(ctx, req.Referer()),
|
||||
"referer", SanitizeURL(ctx, req.Referer()),
|
||||
}
|
||||
|
||||
traceID := tracing.TraceIDFromContext(ctx.Req.Context(), false)
|
||||
@ -74,7 +74,11 @@ func Logger(cfg *setting.Cfg) web.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeURL(ctx *models.ReqContext, s string) string {
|
||||
var sensitiveQueryStrings = [...]string{
|
||||
"auth_token",
|
||||
}
|
||||
|
||||
func SanitizeURL(ctx *models.ReqContext, s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
@ -84,5 +88,13 @@ func sanitizeURL(ctx *models.ReqContext, s string) string {
|
||||
ctx.Logger.Warn("Received invalid referer in request headers, removed for log forgery prevention")
|
||||
return ""
|
||||
}
|
||||
|
||||
// strip out sensitive query strings
|
||||
values := u.Query()
|
||||
for _, query := range sensitiveQueryStrings {
|
||||
values.Del(query)
|
||||
}
|
||||
u.RawQuery = values.Encode()
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ func Test_sanitizeURL(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equalf(t, tt.want, sanitizeURL(tt.args.ctx, tt.args.s), "sanitizeURL(%v, %v)", tt.args.ctx, tt.args.s)
|
||||
assert.Equalf(t, tt.want, SanitizeURL(tt.args.ctx, tt.args.s), "sanitizeURL(%v, %v)", tt.args.ctx, tt.args.s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -493,6 +493,36 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
sc.exec()
|
||||
})
|
||||
|
||||
middlewareScenario(t, "Request body should not be read in default context handler, but query should be altered - jwt", func(t *testing.T, sc *scenarioContext) {
|
||||
sc.fakeReq("POST", "/?targetOrgId=123&auth_token=token")
|
||||
body := "key=value"
|
||||
sc.req.Body = io.NopCloser(strings.NewReader(body))
|
||||
|
||||
sc.handlerFunc = func(c *models.ReqContext) {
|
||||
t.Log("Handler called")
|
||||
defer func() {
|
||||
err := c.Req.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
require.Equal(t, "", c.Req.URL.Query().Get("auth_token"))
|
||||
|
||||
bodyAfterHandler, e := io.ReadAll(c.Req.Body)
|
||||
require.NoError(t, e)
|
||||
require.Equal(t, body, string(bodyAfterHandler))
|
||||
}
|
||||
|
||||
sc.req.Header.Set(sc.cfg.AuthProxyHeaderName, hdrName)
|
||||
sc.req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
sc.req.Header.Set("Content-Length", strconv.Itoa(len(body)))
|
||||
sc.m.Post("/", sc.defaultHandler)
|
||||
sc.exec()
|
||||
}, func(cfg *setting.Cfg) {
|
||||
cfg.JWTAuthEnabled = true
|
||||
cfg.JWTAuthURLLogin = true
|
||||
cfg.JWTAuthHeaderName = "X-WEBAUTH-TOKEN"
|
||||
})
|
||||
|
||||
middlewareScenario(t, "Should get an existing user from header", func(t *testing.T, sc *scenarioContext) {
|
||||
const userID int64 = 12
|
||||
const orgID int64 = 2
|
||||
|
@ -18,6 +18,10 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64)
|
||||
}
|
||||
|
||||
jwtToken := ctx.Req.Header.Get(h.Cfg.JWTAuthHeaderName)
|
||||
if jwtToken == "" && h.Cfg.JWTAuthURLLogin {
|
||||
jwtToken = ctx.Req.URL.Query().Get("auth_token")
|
||||
}
|
||||
|
||||
if jwtToken == "" {
|
||||
return false
|
||||
}
|
||||
|
@ -316,6 +316,7 @@ type Cfg struct {
|
||||
// JWT Auth
|
||||
JWTAuthEnabled bool
|
||||
JWTAuthHeaderName string
|
||||
JWTAuthURLLogin bool
|
||||
JWTAuthEmailClaim string
|
||||
JWTAuthUsernameClaim string
|
||||
JWTAuthExpectClaims string
|
||||
@ -1305,6 +1306,7 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) {
|
||||
authJWT := iniFile.Section("auth.jwt")
|
||||
cfg.JWTAuthEnabled = authJWT.Key("enabled").MustBool(false)
|
||||
cfg.JWTAuthHeaderName = valueAsString(authJWT, "header_name", "")
|
||||
cfg.JWTAuthURLLogin = authJWT.Key("url_login").MustBool(false)
|
||||
cfg.JWTAuthEmailClaim = valueAsString(authJWT, "email_claim", "")
|
||||
cfg.JWTAuthUsernameClaim = valueAsString(authJWT, "username_claim", "")
|
||||
cfg.JWTAuthExpectClaims = valueAsString(authJWT, "expect_claims", "{}")
|
||||
|
@ -17,6 +17,7 @@ import { AppEvents, DataQueryErrorType } from '@grafana/data';
|
||||
import { BackendSrv as BackendService, BackendSrvRequest, config, FetchError, FetchResponse } from '@grafana/runtime';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { getConfig } from 'app/core/config';
|
||||
import { loadUrlToken } from 'app/core/utils/urlToken';
|
||||
import { DashboardSearchHit } from 'app/features/search/types';
|
||||
import { getGrafanaStorage } from 'app/features/storage/storage';
|
||||
import { TokenRevokedModal } from 'app/features/users/TokenRevokedModal';
|
||||
@ -128,6 +129,17 @@ export class BackendSrv implements BackendService {
|
||||
|
||||
options = this.parseRequestOptions(options);
|
||||
|
||||
const token = loadUrlToken();
|
||||
if (token !== null && token !== '') {
|
||||
if (!options.headers) {
|
||||
options.headers = {};
|
||||
}
|
||||
|
||||
if (config.jwtUrlLogin && config.jwtHeaderName) {
|
||||
options.headers[config.jwtHeaderName] = `${token}`;
|
||||
}
|
||||
}
|
||||
|
||||
const fromFetchStream = this.getFromFetchStream<T>(options);
|
||||
const failureStream = fromFetchStream.pipe(this.toFailureStream<T>(options));
|
||||
const successStream = fromFetchStream.pipe(
|
||||
|
13
public/app/core/utils/urlToken.ts
Normal file
13
public/app/core/utils/urlToken.ts
Normal file
@ -0,0 +1,13 @@
|
||||
let cachedToken = '';
|
||||
|
||||
export const loadUrlToken = (): string | null => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
const token = params.get('auth_token');
|
||||
if (token !== null && token !== '') {
|
||||
cachedToken = token;
|
||||
return token;
|
||||
}
|
||||
|
||||
return cachedToken;
|
||||
};
|
@ -18,6 +18,7 @@ import {
|
||||
StreamingFrameOptions,
|
||||
} from '@grafana/runtime/src/services/live';
|
||||
import { BackendDataSourceResponse } from '@grafana/runtime/src/utils/queryResponse';
|
||||
import { loadUrlToken } from 'app/core/utils/urlToken';
|
||||
|
||||
import { StreamingResponseData } from '../data/utils';
|
||||
|
||||
@ -66,7 +67,14 @@ export class CentrifugeService implements CentrifugeSrv {
|
||||
|
||||
constructor(private deps: CentrifugeSrvDeps) {
|
||||
this.dataStreamSubscriberReadiness = deps.dataStreamSubscriberReadiness.pipe(share(), startWith(true));
|
||||
const liveUrl = `${deps.appUrl.replace(/^http/, 'ws')}/api/live/ws`;
|
||||
|
||||
let liveUrl = `${deps.appUrl.replace(/^http/, 'ws')}/api/live/ws`;
|
||||
|
||||
const token = loadUrlToken();
|
||||
if (token !== null && token !== '') {
|
||||
liveUrl += '?auth_token=' + token;
|
||||
}
|
||||
|
||||
this.centrifuge = new Centrifuge(liveUrl, {
|
||||
timeout: 30000,
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user