Auth: Refresh OAuth access_token automatically using the refresh_token (#56076)

* Verify OAuth token expiration for oauth users in the ctx handler middleware

* Use refresh token to get a new access token

* Refactor oauth_token.go

* Add tests for the middleware changes

* Align other tests

* Add tests, wip

* Add more tests

* Add InvalidateOAuthTokens method

* Fix ExpiryDate update to default

* Invalidate OAuth tokens during logout

* Improve logout

* Add more comments

* Cleanup

* Fix import order

* Add error to HasOAuthEntry return values

* add dev debug logs

* Fix tests

Co-authored-by: jguer <joao.guerreiro@grafana.com>
This commit is contained in:
Misi
2022-10-18 18:17:28 +02:00
committed by GitHub
co-authored by jguer
parent 984ec00aac
commit 9c954d06ab
17 changed files with 828 additions and 89 deletions
+1 -1
View File
@@ -213,7 +213,7 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHa
authProxy := authproxy.ProvideAuthProxy(cfg, remoteCacheSvc, loginservice.LoginServiceMock{}, &usertest.FakeUserService{}, sqlStore)
loginService := &logintest.LoginServiceFake{}
authenticator := &logintest.AuthenticatorFake{}
ctxHdlr := contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy, loginService, nil, authenticator, usertest.NewUserServiceFake(), orgtest.NewOrgServiceFake())
ctxHdlr := contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy, loginService, nil, authenticator, usertest.NewUserServiceFake(), orgtest.NewOrgServiceFake(), nil)
return ctxHdlr
}
+4
View File
@@ -16,6 +16,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware/csrf"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/oauthtoken"
"github.com/grafana/grafana/pkg/services/querylibrary"
"github.com/grafana/grafana/pkg/services/searchV2"
"github.com/grafana/grafana/pkg/services/store/object"
@@ -206,6 +207,7 @@ type HTTPServer struct {
annotationsRepo annotations.Repository
tagService tag.Service
userAuthService userauth.Service
oauthTokenService oauthtoken.OAuthTokenService
}
type ServerOptions struct {
@@ -248,6 +250,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
accesscontrolService accesscontrol.Service, dashboardThumbsService thumbs.DashboardThumbService, navTreeService navtree.Service,
annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService,
userAuthService userauth.Service, queryLibraryHTTPService querylibrary.HTTPService, queryLibraryService querylibrary.Service,
oauthTokenService oauthtoken.OAuthTokenService,
) (*HTTPServer, error) {
web.Env = cfg.Env
m := web.New()
@@ -352,6 +355,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
userAuthService: userAuthService,
QueryLibraryHTTPService: queryLibraryHTTPService,
QueryLibraryService: queryLibraryService,
oauthTokenService: oauthTokenService,
}
if hs.Listener != nil {
hs.log.Debug("Using provided listener")
+7
View File
@@ -304,6 +304,13 @@ func (hs *HTTPServer) Logout(c *models.ReqContext) {
}
}
// Invalidate the OAuth tokens in case the User logged in with OAuth or the last external AuthEntry is an OAuth one
if entry, exists, _ := hs.oauthTokenService.HasOAuthEntry(c.Req.Context(), c.SignedInUser); exists {
if err := hs.oauthTokenService.InvalidateOAuthTokens(c.Req.Context(), entry); err != nil {
hs.log.Warn("failed to invalidate oauth tokens for user", "userId", c.UserID, "error", err)
}
}
err := hs.AuthTokenService.RevokeToken(c.Req.Context(), c.UserToken, false)
if err != nil && !errors.Is(err, models.ErrUserTokenNotFound) {
hs.log.Error("failed to revoke auth token", "error", err)
+9 -1
View File
@@ -194,7 +194,15 @@ func (hs *HTTPServer) OAuthLogin(ctx *models.ReqContext) {
// token.TokenType was defaulting to "bearer", which is out of spec, so we explicitly set to "Bearer"
token.TokenType = "Bearer"
oauthLogger.Debug("OAuthLogin: got token", "expiry", fmt.Sprintf("%v", token.Expiry))
if hs.Cfg.Env != setting.Dev {
oauthLogger.Debug("OAuthLogin: got token", "expiry", fmt.Sprintf("%v", token.Expiry))
} else {
oauthLogger.Debug("OAuthLogin: got token",
"expiry", fmt.Sprintf("%v", token.Expiry),
"access_token", fmt.Sprintf("%v", token.AccessToken),
"refresh_token", fmt.Sprintf("%v", token.RefreshToken),
)
}
// set up oauth2 client
client := connect.Client(oauthCtx, token)
+13
View File
@@ -15,6 +15,7 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
pluginClient "github.com/grafana/grafana/pkg/plugins/manager/client"
@@ -56,6 +57,18 @@ func (ts *fakeOAuthTokenService) IsOAuthPassThruEnabled(*datasources.DataSource)
return ts.passThruEnabled
}
func (ts *fakeOAuthTokenService) HasOAuthEntry(context.Context, *user.SignedInUser) (*models.UserAuth, bool, error) {
return nil, false, nil
}
func (ts *fakeOAuthTokenService) TryTokenRefresh(ctx context.Context, usr *models.UserAuth) error {
return nil
}
func (ts *fakeOAuthTokenService) InvalidateOAuthTokens(ctx context.Context, usr *models.UserAuth) error {
return nil
}
// `/ds/query` endpoint test
func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) {
qds := query.ProvideService(
+12
View File
@@ -1065,3 +1065,15 @@ func (m *mockOAuthTokenService) GetCurrentOAuthToken(ctx context.Context, user *
func (m *mockOAuthTokenService) IsOAuthPassThruEnabled(ds *datasources.DataSource) bool {
return m.oAuthEnabled
}
func (m *mockOAuthTokenService) HasOAuthEntry(context.Context, *user.SignedInUser) (*models.UserAuth, bool, error) {
return nil, false, nil
}
func (m *mockOAuthTokenService) TryTokenRefresh(context.Context, *models.UserAuth) error {
return nil
}
func (m *mockOAuthTokenService) InvalidateOAuthTokens(context.Context, *models.UserAuth) error {
return nil
}