Browse Source

Merge pull request #14912 from grafana/poc_token_auth

WIP. POC for session replacement base hashed tokens.
Carl Bergquist 7 years ago
parent
commit
d5c1f8e526

+ 19 - 5
conf/defaults.ini

@@ -106,6 +106,22 @@ path = grafana.db
 # For "sqlite3" only. cache mode setting used for connecting to the database
 # For "sqlite3" only. cache mode setting used for connecting to the database
 cache_mode = private
 cache_mode = private
 
 
+#################################### Login ###############################
+
+[login]
+
+# Login cookie name
+cookie_name = grafana_session
+
+# How many days an session can be unused before we inactivate it
+login_remember_days = 7
+
+# How often should the login token be rotated. default to '10m'
+rotate_token_minutes = 10
+
+# How long should Grafana keep expired tokens before deleting them
+delete_expired_token_after_days = 30
+
 #################################### Session #############################
 #################################### Session #############################
 [session]
 [session]
 # Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file"
 # Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file"
@@ -175,11 +191,6 @@ admin_password = admin
 # used for signing
 # used for signing
 secret_key = SW2YcwTIb9zpOOhoPsMm
 secret_key = SW2YcwTIb9zpOOhoPsMm
 
 
-# Auto-login remember days
-login_remember_days = 7
-cookie_username = grafana_user
-cookie_remember_name = grafana_remember
-
 # disable gravatar profile images
 # disable gravatar profile images
 disable_gravatar = false
 disable_gravatar = false
 
 
@@ -189,6 +200,9 @@ data_source_proxy_whitelist =
 # disable protection against brute force login attempts
 # disable protection against brute force login attempts
 disable_brute_force_login_protection = false
 disable_brute_force_login_protection = false
 
 
+# set cookies as https only. default is false
+https_flag_cookies = false
+
 #################################### Snapshots ###########################
 #################################### Snapshots ###########################
 [snapshots]
 [snapshots]
 # snapshot sharing options
 # snapshot sharing options

+ 19 - 5
conf/sample.ini

@@ -102,6 +102,22 @@ log_queries =
 # For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared)
 # For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared)
 ;cache_mode = private
 ;cache_mode = private
 
 
+#################################### Login ###############################
+
+[login]
+
+# Login cookie name
+;cookie_name = grafana_session
+
+# How many days an session can be unused before we inactivate it
+;login_remember_days = 7
+
+# How often should the login token be rotated. default to '10'
+;rotate_token_minutes = 10
+
+# How long should Grafana keep expired tokens before deleting them
+;delete_expired_token_after_days = 30
+
 #################################### Session ####################################
 #################################### Session ####################################
 [session]
 [session]
 # Either "memory", "file", "redis", "mysql", "postgres", default is "file"
 # Either "memory", "file", "redis", "mysql", "postgres", default is "file"
@@ -162,11 +178,6 @@ log_queries =
 # used for signing
 # used for signing
 ;secret_key = SW2YcwTIb9zpOOhoPsMm
 ;secret_key = SW2YcwTIb9zpOOhoPsMm
 
 
-# Auto-login remember days
-;login_remember_days = 7
-;cookie_username = grafana_user
-;cookie_remember_name = grafana_remember
-
 # disable gravatar profile images
 # disable gravatar profile images
 ;disable_gravatar = false
 ;disable_gravatar = false
 
 
@@ -176,6 +187,9 @@ log_queries =
 # disable protection against brute force login attempts
 # disable protection against brute force login attempts
 ;disable_brute_force_login_protection = false
 ;disable_brute_force_login_protection = false
 
 
+# set cookies as https only. default is false
+;https_flag_cookies = false
+
 #################################### Snapshots ###########################
 #################################### Snapshots ###########################
 [snapshots]
 [snapshots]
 # snapshot sharing options
 # snapshot sharing options

+ 2 - 1
devenv/docker/ha_test/docker-compose.yaml

@@ -54,7 +54,8 @@ services:
       # - GF_DATABASE_SSL_MODE=disable
       # - GF_DATABASE_SSL_MODE=disable
       # - GF_SESSION_PROVIDER=postgres
       # - GF_SESSION_PROVIDER=postgres
       # - GF_SESSION_PROVIDER_CONFIG=user=grafana password=password host=db port=5432 dbname=grafana sslmode=disable
       # - GF_SESSION_PROVIDER_CONFIG=user=grafana password=password host=db port=5432 dbname=grafana sslmode=disable
-      - GF_LOG_FILTERS=alerting.notifier:debug,alerting.notifier.slack:debug
+      - GF_LOG_FILTERS=alerting.notifier:debug,alerting.notifier.slack:debug,auth:debug
+      - GF_LOGIN_ROTATE_TOKEN_MINUTES=2
     ports:
     ports:
       - 3000
       - 3000
     depends_on:
     depends_on:

+ 69 - 0
devenv/docker/loadtest/README.md

@@ -0,0 +1,69 @@
+# Grafana load test
+
+Runs load tests and checks using [k6](https://k6.io/).
+
+## Prerequisites
+
+Docker
+
+## Run
+
+Run load test for 15 minutes:
+
+```bash
+$ ./run.sh
+```
+
+Run load test for custom duration:
+
+```bash
+$ ./run.sh -d 10s
+```
+
+Example output:
+
+```bash
+
+          /\      |‾‾|  /‾‾/  /‾/
+     /\  /  \     |  |_/  /  / /
+    /  \/    \    |      |  /  ‾‾\
+   /          \   |  |‾\  \ | (_) |
+  / __________ \  |__|  \__\ \___/ .io
+
+  execution: local
+     output: -
+     script: src/auth_token_test.js
+
+    duration: 15m0s, iterations: -
+         vus: 2,     max: 2
+
+    done [==========================================================] 15m0s / 15m0s
+
+    █ user auth token test
+
+      █ user authenticates thru ui with username and password
+
+        ✓ response status is 200
+        ✓ response has cookie 'grafana_session' with 32 characters
+
+      █ batch tsdb requests
+
+        ✓ response status is 200
+
+    checks.....................: 100.00% ✓ 32844 ✗ 0
+    data_received..............: 411 MB  457 kB/s
+    data_sent..................: 12 MB   14 kB/s
+    group_duration.............: avg=95.64ms  min=16.42ms  med=94.35ms  max=307.52ms p(90)=137.78ms p(95)=146.75ms
+    http_req_blocked...........: avg=1.27ms   min=942ns    med=610.08µs max=48.32ms  p(90)=2.92ms   p(95)=4.25ms
+    http_req_connecting........: avg=1.06ms   min=0s       med=456.79µs max=47.19ms  p(90)=2.55ms   p(95)=3.78ms
+    http_req_duration..........: avg=58.16ms  min=1ms      med=52.59ms  max=293.35ms p(90)=109.53ms p(95)=120.19ms
+    http_req_receiving.........: avg=38.98µs  min=6.43µs   med=32.55µs  max=16.2ms   p(90)=64.63µs  p(95)=78.8µs
+    http_req_sending...........: avg=328.66µs min=8.09µs   med=110.77µs max=44.13ms  p(90)=552.65µs p(95)=1.09ms
+    http_req_tls_handshaking...: avg=0s       min=0s       med=0s       max=0s       p(90)=0s       p(95)=0s
+    http_req_waiting...........: avg=57.79ms  min=935.02µs med=52.15ms  max=293.06ms p(90)=109.04ms p(95)=119.71ms
+    http_reqs..................: 34486   38.317775/s
+    iteration_duration.........: avg=1.09s    min=1.81µs   med=1.09s    max=1.3s     p(90)=1.13s    p(95)=1.14s
+    iterations.................: 1642    1.824444/s
+    vus........................: 2       min=2   max=2
+    vus_max....................: 2       min=2   max=2
+```

+ 71 - 0
devenv/docker/loadtest/auth_token_test.js

@@ -0,0 +1,71 @@
+import { sleep, check, group } from 'k6';
+import { createClient, createBasicAuthClient } from './modules/client.js';
+import { createTestOrgIfNotExists, createTestdataDatasourceIfNotExists } from './modules/util.js';
+
+export let options = {
+  noCookiesReset: true
+};
+
+let endpoint = __ENV.URL || 'http://localhost:3000';
+const client = createClient(endpoint);
+
+export const setup = () => {
+  const basicAuthClient = createBasicAuthClient(endpoint, 'admin', 'admin');
+  const orgId = createTestOrgIfNotExists(basicAuthClient);
+  const datasourceId = createTestdataDatasourceIfNotExists(basicAuthClient);
+  client.withOrgId(orgId);
+  return {
+    orgId: orgId,
+    datasourceId: datasourceId,
+  };
+}
+
+export default (data) => {
+  group("user auth token test", () => {
+    if (__ITER === 0) {
+      group("user authenticates thru ui with username and password", () => {
+        let res = client.ui.login('admin', 'admin');
+
+        check(res, {
+          'response status is 200': (r) => r.status === 200,
+          'response has cookie \'grafana_session\' with 32 characters': (r) => r.cookies.grafana_session[0].value.length === 32,
+        });
+      });
+    }
+
+    if (__ITER !== 0) {
+      group("batch tsdb requests", () => {
+        const batchCount = 20;
+        const requests = [];
+        const payload = {
+          from: '1547765247624',
+          to: '1547768847624',
+          queries: [{
+            refId: 'A',
+            scenarioId: 'random_walk',
+            intervalMs: 10000,
+            maxDataPoints: 433,
+            datasourceId: data.datasourceId,
+          }]
+        };
+
+        requests.push({ method: 'GET', url: '/api/annotations?dashboardId=2074&from=1548078832772&to=1548082432772' });
+
+        for (let n = 0; n < batchCount; n++) {
+          requests.push({ method: 'POST', url: '/api/tsdb/query', body: payload });
+        }
+
+        let responses = client.batch(requests);
+        for (let n = 0; n < batchCount; n++) {
+          check(responses[n], {
+            'response status is 200': (r) => r.status === 200,
+          });
+        }
+      });
+    }
+  });
+
+  sleep(1)
+}
+
+export const teardown = (data) => {}

+ 187 - 0
devenv/docker/loadtest/modules/client.js

@@ -0,0 +1,187 @@
+import http from "k6/http";
+import encoding from 'k6/encoding';
+
+export const UIEndpoint = class UIEndpoint {
+  constructor(httpClient) {
+    this.httpClient = httpClient;
+  }
+
+  login(username, pwd) {
+    const payload = { user: username, password: pwd };
+    return this.httpClient.formPost('/login', payload);
+  }
+}
+
+export const DatasourcesEndpoint = class DatasourcesEndpoint {
+  constructor(httpClient) {
+    this.httpClient = httpClient;
+  }
+
+  getById(id) {
+    return this.httpClient.get(`/datasources/${id}`);
+  }
+
+  getByName(name) {
+    return this.httpClient.get(`/datasources/name/${name}`);
+  }
+
+  create(payload) {
+    return this.httpClient.post(`/datasources`, JSON.stringify(payload));
+  }
+
+  delete(id) {
+    return this.httpClient.delete(`/datasources/${id}`);
+  }
+}
+
+export const OrganizationsEndpoint = class OrganizationsEndpoint {
+  constructor(httpClient) {
+    this.httpClient = httpClient;
+  }
+
+  getById(id) {
+    return this.httpClient.get(`/orgs/${id}`);
+  }
+
+  getByName(name) {
+    return this.httpClient.get(`/orgs/name/${name}`);
+  }
+
+  create(name) {
+    let payload = {
+      name: name,
+    };
+    return this.httpClient.post(`/orgs`, JSON.stringify(payload));
+  }
+
+  delete(id) {
+    return this.httpClient.delete(`/orgs/${id}`);
+  }
+}
+
+export const GrafanaClient = class GrafanaClient {
+  constructor(httpClient) {
+    httpClient.onBeforeRequest = this.onBeforeRequest;
+    this.raw = httpClient;
+    this.ui = new UIEndpoint(httpClient);
+    this.orgs = new OrganizationsEndpoint(httpClient.withUrl('/api'));
+    this.datasources = new DatasourcesEndpoint(httpClient.withUrl('/api'));
+  }
+
+  batch(requests) {
+    return this.raw.batch(requests);
+  }
+
+  withOrgId(orgId) {
+    this.orgId = orgId;
+  }
+
+  onBeforeRequest(params) {
+    if (this.orgId && this.orgId > 0) {
+      params = params.headers || {};
+      params.headers["X-Grafana-Org-Id"] = this.orgId;
+    }
+  }
+}
+
+export const BaseClient = class BaseClient {
+  constructor(url, subUrl) {
+    if (url.endsWith('/')) {
+      url = url.substring(0, url.length - 1);
+    }
+
+    if (subUrl.endsWith('/')) {
+      subUrl = subUrl.substring(0, subUrl.length - 1);
+    }
+
+    this.url = url + subUrl;
+    this.onBeforeRequest = () => {};
+  }
+
+  withUrl(subUrl) {
+    let c = new BaseClient(this.url,  subUrl);
+    c.onBeforeRequest = this.onBeforeRequest;
+    return c;
+  }
+
+  beforeRequest(params) {
+
+  }
+
+  get(url, params) {
+    params = params || {};
+    this.beforeRequest(params);
+    this.onBeforeRequest(params);
+    return http.get(this.url + url, params);
+  }
+
+  formPost(url, body, params) {
+    params = params || {};
+    this.beforeRequest(params);
+    this.onBeforeRequest(params);
+    return http.post(this.url + url, body, params);
+  }
+
+  post(url, body, params) {
+    params = params || {};
+    params.headers = params.headers || {};
+    params.headers['Content-Type'] = 'application/json';
+
+    this.beforeRequest(params);
+    this.onBeforeRequest(params);
+    return http.post(this.url + url, body, params);
+  }
+
+  delete(url, params) {
+    params = params || {};
+    this.beforeRequest(params);
+    this.onBeforeRequest(params);
+    return http.del(this.url + url, null, params);
+  }
+
+  batch(requests) {
+    for (let n = 0; n < requests.length; n++) {
+      let params = requests[n].params || {};
+      params.headers = params.headers || {};
+      params.headers['Content-Type'] = 'application/json';
+      this.beforeRequest(params);
+      this.onBeforeRequest(params);
+      requests[n].params = params;
+      requests[n].url = this.url + requests[n].url;
+      if (requests[n].body) {
+        requests[n].body = JSON.stringify(requests[n].body);
+      }
+    }
+
+    return http.batch(requests);
+  }
+}
+
+export class BasicAuthClient extends BaseClient {
+  constructor(url, subUrl, username, password) {
+    super(url, subUrl);
+    this.username = username;
+    this.password = password;
+  }
+
+  withUrl(subUrl) {
+    let c = new BasicAuthClient(this.url,  subUrl, this.username, this.password);
+    c.onBeforeRequest = this.onBeforeRequest;
+    return c;
+  }
+
+  beforeRequest(params) {
+    params = params || {};
+    params.headers = params.headers || {};
+    let token = `${this.username}:${this.password}`;
+    params.headers['Authorization'] = `Basic ${encoding.b64encode(token)}`;
+  }
+}
+
+export const createClient = (url) => {
+  return new GrafanaClient(new BaseClient(url, ''));
+}
+
+export const createBasicAuthClient = (url, username, password) => {
+  return new GrafanaClient(new BasicAuthClient(url, '', username, password));
+}

+ 35 - 0
devenv/docker/loadtest/modules/util.js

@@ -0,0 +1,35 @@
+export const createTestOrgIfNotExists = (client) => {
+  let orgId = 0;
+  let res = client.orgs.getByName('k6');
+  if (res.status === 404) {
+    res = client.orgs.create('k6');
+    if (res.status !== 200) {
+      throw new Error('Expected 200 response status when creating org');
+    }
+    orgId = res.json().orgId;
+  } else {
+    orgId = res.json().id;
+  }
+
+  client.withOrgId(orgId);
+  return orgId;
+}
+
+export const createTestdataDatasourceIfNotExists = (client) => {
+  const payload = {
+    access: 'proxy',
+    isDefault: false,
+    name: 'k6-testdata',
+    type: 'testdata',
+  };
+
+  let res = client.datasources.getByName(payload.name);
+  if (res.status === 404) {
+    res = client.datasources.create(payload);
+    if (res.status !== 200) {
+      throw new Error('Expected 200 response status when creating datasource');
+    }
+  }
+
+  return res.json().id;
+}

+ 24 - 0
devenv/docker/loadtest/run.sh

@@ -0,0 +1,24 @@
+#/bin/bash
+
+PWD=$(pwd)
+
+run() {
+  duration='15m'
+  url='http://localhost:3000'
+
+  while getopts ":d:u:" o; do
+    case "${o}" in
+				d)
+            duration=${OPTARG}
+            ;;
+        u)
+            url=${OPTARG}
+            ;;
+    esac
+	done
+	shift $((OPTIND-1))
+
+  docker run -t --network=host -v $PWD:/src -e URL=$url --rm -i loadimpact/k6:master run --vus 2 --duration $duration src/auth_token_test.js
+}
+
+run "$@"

+ 6 - 6
pkg/api/api.go

@@ -23,9 +23,9 @@ func (hs *HTTPServer) registerRoutes() {
 
 
 	// not logged in views
 	// not logged in views
 	r.Get("/", reqSignedIn, hs.Index)
 	r.Get("/", reqSignedIn, hs.Index)
-	r.Get("/logout", Logout)
-	r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), Wrap(LoginPost))
-	r.Get("/login/:name", quota("session"), OAuthLogin)
+	r.Get("/logout", hs.Logout)
+	r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), Wrap(hs.LoginPost))
+	r.Get("/login/:name", quota("session"), hs.OAuthLogin)
 	r.Get("/login", hs.LoginView)
 	r.Get("/login", hs.LoginView)
 	r.Get("/invite/:code", hs.Index)
 	r.Get("/invite/:code", hs.Index)
 
 
@@ -84,11 +84,11 @@ func (hs *HTTPServer) registerRoutes() {
 	r.Get("/signup", hs.Index)
 	r.Get("/signup", hs.Index)
 	r.Get("/api/user/signup/options", Wrap(GetSignUpOptions))
 	r.Get("/api/user/signup/options", Wrap(GetSignUpOptions))
 	r.Post("/api/user/signup", quota("user"), bind(dtos.SignUpForm{}), Wrap(SignUp))
 	r.Post("/api/user/signup", quota("user"), bind(dtos.SignUpForm{}), Wrap(SignUp))
-	r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), Wrap(SignUpStep2))
+	r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), Wrap(hs.SignUpStep2))
 
 
 	// invited
 	// invited
 	r.Get("/api/user/invite/:code", Wrap(GetInviteInfoByCode))
 	r.Get("/api/user/invite/:code", Wrap(GetInviteInfoByCode))
-	r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), Wrap(CompleteInvite))
+	r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), Wrap(hs.CompleteInvite))
 
 
 	// reset password
 	// reset password
 	r.Get("/user/password/send-reset-email", hs.Index)
 	r.Get("/user/password/send-reset-email", hs.Index)
@@ -109,7 +109,7 @@ func (hs *HTTPServer) registerRoutes() {
 	r.Delete("/api/snapshots/:key", reqEditorRole, Wrap(DeleteDashboardSnapshot))
 	r.Delete("/api/snapshots/:key", reqEditorRole, Wrap(DeleteDashboardSnapshot))
 
 
 	// api renew session based on remember cookie
 	// api renew session based on remember cookie
-	r.Get("/api/login/ping", quota("session"), LoginAPIPing)
+	r.Get("/api/login/ping", quota("session"), hs.LoginAPIPing)
 
 
 	// authed api
 	// authed api
 	r.Group("/api", func(apiRoute routing.RouteRegister) {
 	r.Group("/api", func(apiRoute routing.RouteRegister) {

+ 32 - 10
pkg/api/common_test.go

@@ -5,7 +5,6 @@ import (
 	"net/http/httptest"
 	"net/http/httptest"
 	"path/filepath"
 	"path/filepath"
 
 
-	"github.com/go-macaron/session"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
@@ -95,13 +94,14 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map
 }
 }
 
 
 type scenarioContext struct {
 type scenarioContext struct {
-	m              *macaron.Macaron
-	context        *m.ReqContext
-	resp           *httptest.ResponseRecorder
-	handlerFunc    handlerFunc
-	defaultHandler macaron.Handler
-	req            *http.Request
-	url            string
+	m                    *macaron.Macaron
+	context              *m.ReqContext
+	resp                 *httptest.ResponseRecorder
+	handlerFunc          handlerFunc
+	defaultHandler       macaron.Handler
+	req                  *http.Request
+	url                  string
+	userAuthTokenService *fakeUserAuthTokenService
 }
 }
 
 
 func (sc *scenarioContext) exec() {
 func (sc *scenarioContext) exec() {
@@ -123,8 +123,30 @@ func setupScenarioContext(url string) *scenarioContext {
 		Delims:    macaron.Delims{Left: "[[", Right: "]]"},
 		Delims:    macaron.Delims{Left: "[[", Right: "]]"},
 	}))
 	}))
 
 
-	sc.m.Use(middleware.GetContextHandler())
-	sc.m.Use(middleware.Sessioner(&session.Options{}, 0))
+	sc.userAuthTokenService = newFakeUserAuthTokenService()
+	sc.m.Use(middleware.GetContextHandler(sc.userAuthTokenService))
 
 
 	return sc
 	return sc
 }
 }
+
+type fakeUserAuthTokenService struct {
+	initContextWithTokenProvider func(ctx *m.ReqContext, orgID int64) bool
+}
+
+func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
+	return &fakeUserAuthTokenService{
+		initContextWithTokenProvider: func(ctx *m.ReqContext, orgID int64) bool {
+			return false
+		},
+	}
+}
+
+func (s *fakeUserAuthTokenService) InitContextWithToken(ctx *m.ReqContext, orgID int64) bool {
+	return s.initContextWithTokenProvider(ctx, orgID)
+}
+
+func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqContext) error {
+	return nil
+}
+
+func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) {}

+ 17 - 16
pkg/api/http_server.go

@@ -11,14 +11,8 @@ import (
 	"path"
 	"path"
 	"time"
 	"time"
 
 
-	"github.com/grafana/grafana/pkg/api/routing"
-	"github.com/prometheus/client_golang/prometheus"
-
-	"github.com/prometheus/client_golang/prometheus/promhttp"
-
-	macaron "gopkg.in/macaron.v1"
-
 	"github.com/grafana/grafana/pkg/api/live"
 	"github.com/grafana/grafana/pkg/api/live"
+	"github.com/grafana/grafana/pkg/api/routing"
 	httpstatic "github.com/grafana/grafana/pkg/api/static"
 	httpstatic "github.com/grafana/grafana/pkg/api/static"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
@@ -27,11 +21,16 @@ import (
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/registry"
 	"github.com/grafana/grafana/pkg/registry"
+	"github.com/grafana/grafana/pkg/services/auth"
 	"github.com/grafana/grafana/pkg/services/cache"
 	"github.com/grafana/grafana/pkg/services/cache"
 	"github.com/grafana/grafana/pkg/services/datasources"
 	"github.com/grafana/grafana/pkg/services/datasources"
 	"github.com/grafana/grafana/pkg/services/hooks"
 	"github.com/grafana/grafana/pkg/services/hooks"
 	"github.com/grafana/grafana/pkg/services/rendering"
 	"github.com/grafana/grafana/pkg/services/rendering"
+	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promhttp"
+	macaron "gopkg.in/macaron.v1"
 )
 )
 
 
 func init() {
 func init() {
@@ -49,13 +48,14 @@ type HTTPServer struct {
 	streamManager *live.StreamManager
 	streamManager *live.StreamManager
 	httpSrv       *http.Server
 	httpSrv       *http.Server
 
 
-	RouteRegister   routing.RouteRegister    `inject:""`
-	Bus             bus.Bus                  `inject:""`
-	RenderService   rendering.Service        `inject:""`
-	Cfg             *setting.Cfg             `inject:""`
-	HooksService    *hooks.HooksService      `inject:""`
-	CacheService    *cache.CacheService      `inject:""`
-	DatasourceCache datasources.CacheService `inject:""`
+	RouteRegister    routing.RouteRegister     `inject:""`
+	Bus              bus.Bus                   `inject:""`
+	RenderService    rendering.Service         `inject:""`
+	Cfg              *setting.Cfg              `inject:""`
+	HooksService     *hooks.HooksService       `inject:""`
+	CacheService     *cache.CacheService       `inject:""`
+	DatasourceCache  datasources.CacheService  `inject:""`
+	AuthTokenService auth.UserAuthTokenService `inject:""`
 }
 }
 
 
 func (hs *HTTPServer) Init() error {
 func (hs *HTTPServer) Init() error {
@@ -65,6 +65,8 @@ func (hs *HTTPServer) Init() error {
 	hs.macaron = hs.newMacaron()
 	hs.macaron = hs.newMacaron()
 	hs.registerRoutes()
 	hs.registerRoutes()
 
 
+	session.Init(&setting.SessionOptions, setting.SessionConnMaxLifetime)
+
 	return nil
 	return nil
 }
 }
 
 
@@ -223,8 +225,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
 
 
 	m.Use(hs.healthHandler)
 	m.Use(hs.healthHandler)
 	m.Use(hs.metricsEndpoint)
 	m.Use(hs.metricsEndpoint)
-	m.Use(middleware.GetContextHandler())
-	m.Use(middleware.Sessioner(&setting.SessionOptions, setting.SessionConnMaxLifetime))
+	m.Use(middleware.GetContextHandler(hs.AuthTokenService))
 	m.Use(middleware.OrgRedirect())
 	m.Use(middleware.OrgRedirect())
 
 
 	// needs to be after context handler
 	// needs to be after context handler

+ 59 - 67
pkg/api/login.go

@@ -1,6 +1,8 @@
 package api
 package api
 
 
 import (
 import (
+	"encoding/hex"
+	"net/http"
 	"net/url"
 	"net/url"
 
 
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/api/dtos"
@@ -9,12 +11,13 @@ import (
 	"github.com/grafana/grafana/pkg/login"
 	"github.com/grafana/grafana/pkg/login"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/metrics"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
+	"github.com/grafana/grafana/pkg/util"
 )
 )
 
 
 const (
 const (
-	ViewIndex = "index"
+	ViewIndex            = "index"
+	LoginErrorCookieName = "login_error"
 )
 )
 
 
 func (hs *HTTPServer) LoginView(c *m.ReqContext) {
 func (hs *HTTPServer) LoginView(c *m.ReqContext) {
@@ -34,8 +37,8 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
 	viewData.Settings["loginHint"] = setting.LoginHint
 	viewData.Settings["loginHint"] = setting.LoginHint
 	viewData.Settings["disableLoginForm"] = setting.DisableLoginForm
 	viewData.Settings["disableLoginForm"] = setting.DisableLoginForm
 
 
-	if loginError, ok := c.Session.Get("loginError").(string); ok {
-		c.Session.Delete("loginError")
+	if loginError, ok := tryGetEncryptedCookie(c, LoginErrorCookieName); ok {
+		deleteCookie(c, LoginErrorCookieName)
 		viewData.Settings["loginError"] = loginError
 		viewData.Settings["loginError"] = loginError
 	}
 	}
 
 
@@ -43,7 +46,7 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
 		return
 		return
 	}
 	}
 
 
-	if !tryLoginUsingRememberCookie(c) {
+	if !c.IsSignedIn {
 		c.HTML(200, ViewIndex, viewData)
 		c.HTML(200, ViewIndex, viewData)
 		return
 		return
 	}
 	}
@@ -75,56 +78,15 @@ func tryOAuthAutoLogin(c *m.ReqContext) bool {
 	return false
 	return false
 }
 }
 
 
-func tryLoginUsingRememberCookie(c *m.ReqContext) bool {
-	// Check auto-login.
-	uname := c.GetCookie(setting.CookieUserName)
-	if len(uname) == 0 {
-		return false
-	}
-
-	isSucceed := false
-	defer func() {
-		if !isSucceed {
-			log.Trace("auto-login cookie cleared: %s", uname)
-			c.SetCookie(setting.CookieUserName, "", -1, setting.AppSubUrl+"/")
-			c.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubUrl+"/")
-			return
-		}
-	}()
-
-	userQuery := m.GetUserByLoginQuery{LoginOrEmail: uname}
-	if err := bus.Dispatch(&userQuery); err != nil {
-		return false
-	}
-
-	user := userQuery.Result
-
-	// validate remember me cookie
-	signingKey := user.Rands + user.Password
-	if len(signingKey) < 10 {
-		c.Logger.Error("Invalid user signingKey")
-		return false
+func (hs *HTTPServer) LoginAPIPing(c *m.ReqContext) Response {
+	if c.IsSignedIn || c.IsAnonymous {
+		return JSON(200, "Logged in")
 	}
 	}
 
 
-	if val, _ := c.GetSuperSecureCookie(signingKey, setting.CookieRememberName); val != user.Login {
-		return false
-	}
-
-	isSucceed = true
-	loginUserWithUser(user, c)
-	return true
+	return Error(401, "Unauthorized", nil)
 }
 }
 
 
-func LoginAPIPing(c *m.ReqContext) {
-	if !tryLoginUsingRememberCookie(c) {
-		c.JsonApiErr(401, "Unauthorized", nil)
-		return
-	}
-
-	c.JsonOK("Logged in")
-}
-
-func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
+func (hs *HTTPServer) LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
 	if setting.DisableLoginForm {
 	if setting.DisableLoginForm {
 		return Error(401, "Login is disabled", nil)
 		return Error(401, "Login is disabled", nil)
 	}
 	}
@@ -146,7 +108,7 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
 
 
 	user := authQuery.User
 	user := authQuery.User
 
 
-	loginUserWithUser(user, c)
+	hs.loginUserWithUser(user, c)
 
 
 	result := map[string]interface{}{
 	result := map[string]interface{}{
 		"message": "Logged in",
 		"message": "Logged in",
@@ -162,30 +124,60 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
 	return JSON(200, result)
 	return JSON(200, result)
 }
 }
 
 
-func loginUserWithUser(user *m.User, c *m.ReqContext) {
+func (hs *HTTPServer) loginUserWithUser(user *m.User, c *m.ReqContext) {
 	if user == nil {
 	if user == nil {
-		log.Error(3, "User login with nil user")
+		hs.log.Error("User login with nil user")
 	}
 	}
 
 
-	c.Resp.Header().Del("Set-Cookie")
-
-	days := 86400 * setting.LogInRememberDays
-	if days > 0 {
-		c.SetCookie(setting.CookieUserName, user.Login, days, setting.AppSubUrl+"/")
-		c.SetSuperSecureCookie(user.Rands+user.Password, setting.CookieRememberName, user.Login, days, setting.AppSubUrl+"/")
+	err := hs.AuthTokenService.UserAuthenticatedHook(user, c)
+	if err != nil {
+		hs.log.Error("User auth hook failed", "error", err)
 	}
 	}
-
-	c.Session.RegenerateId(c.Context)
-	c.Session.Set(session.SESS_KEY_USERID, user.Id)
 }
 }
 
 
-func Logout(c *m.ReqContext) {
-	c.SetCookie(setting.CookieUserName, "", -1, setting.AppSubUrl+"/")
-	c.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubUrl+"/")
-	c.Session.Destory(c.Context)
+func (hs *HTTPServer) Logout(c *m.ReqContext) {
+	hs.AuthTokenService.UserSignedOutHook(c)
+
 	if setting.SignoutRedirectUrl != "" {
 	if setting.SignoutRedirectUrl != "" {
 		c.Redirect(setting.SignoutRedirectUrl)
 		c.Redirect(setting.SignoutRedirectUrl)
 	} else {
 	} else {
 		c.Redirect(setting.AppSubUrl + "/login")
 		c.Redirect(setting.AppSubUrl + "/login")
 	}
 	}
 }
 }
+
+func tryGetEncryptedCookie(ctx *m.ReqContext, cookieName string) (string, bool) {
+	cookie := ctx.GetCookie(cookieName)
+	if cookie == "" {
+		return "", false
+	}
+
+	decoded, err := hex.DecodeString(cookie)
+	if err != nil {
+		return "", false
+	}
+
+	decryptedError, err := util.Decrypt([]byte(decoded), setting.SecretKey)
+	return string(decryptedError), err == nil
+}
+
+func deleteCookie(ctx *m.ReqContext, cookieName string) {
+	ctx.SetCookie(cookieName, "", -1, setting.AppSubUrl+"/")
+}
+
+func (hs *HTTPServer) trySetEncryptedCookie(ctx *m.ReqContext, cookieName string, value string, maxAge int) error {
+	encryptedError, err := util.Encrypt([]byte(value), setting.SecretKey)
+	if err != nil {
+		return err
+	}
+
+	http.SetCookie(ctx.Resp, &http.Cookie{
+		Name:     cookieName,
+		MaxAge:   60,
+		Value:    hex.EncodeToString(encryptedError),
+		HttpOnly: true,
+		Path:     setting.AppSubUrl + "/",
+		Secure:   hs.Cfg.SecurityHTTPSCookies,
+	})
+
+	return nil
+}

+ 49 - 16
pkg/api/login_oauth.go

@@ -3,9 +3,11 @@ package api
 import (
 import (
 	"context"
 	"context"
 	"crypto/rand"
 	"crypto/rand"
+	"crypto/sha256"
 	"crypto/tls"
 	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509"
 	"encoding/base64"
 	"encoding/base64"
+	"encoding/hex"
 	"fmt"
 	"fmt"
 	"io/ioutil"
 	"io/ioutil"
 	"net/http"
 	"net/http"
@@ -18,12 +20,14 @@ import (
 	"github.com/grafana/grafana/pkg/login"
 	"github.com/grafana/grafana/pkg/login"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/metrics"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/social"
 	"github.com/grafana/grafana/pkg/social"
 )
 )
 
 
-var oauthLogger = log.New("oauth")
+var (
+	oauthLogger          = log.New("oauth")
+	OauthStateCookieName = "oauth_state"
+)
 
 
 func GenStateString() string {
 func GenStateString() string {
 	rnd := make([]byte, 32)
 	rnd := make([]byte, 32)
@@ -31,7 +35,7 @@ func GenStateString() string {
 	return base64.URLEncoding.EncodeToString(rnd)
 	return base64.URLEncoding.EncodeToString(rnd)
 }
 }
 
 
-func OAuthLogin(ctx *m.ReqContext) {
+func (hs *HTTPServer) OAuthLogin(ctx *m.ReqContext) {
 	if setting.OAuthService == nil {
 	if setting.OAuthService == nil {
 		ctx.Handle(404, "OAuth not enabled", nil)
 		ctx.Handle(404, "OAuth not enabled", nil)
 		return
 		return
@@ -48,14 +52,15 @@ func OAuthLogin(ctx *m.ReqContext) {
 	if errorParam != "" {
 	if errorParam != "" {
 		errorDesc := ctx.Query("error_description")
 		errorDesc := ctx.Query("error_description")
 		oauthLogger.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc)
 		oauthLogger.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc)
-		redirectWithError(ctx, login.ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
+		hs.redirectWithError(ctx, login.ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
 		return
 		return
 	}
 	}
 
 
 	code := ctx.Query("code")
 	code := ctx.Query("code")
 	if code == "" {
 	if code == "" {
 		state := GenStateString()
 		state := GenStateString()
-		ctx.Session.Set(session.SESS_KEY_OAUTH_STATE, state)
+		hashedState := hashStatecode(state, setting.OAuthService.OAuthInfos[name].ClientSecret)
+		hs.writeCookie(ctx.Resp, OauthStateCookieName, hashedState, 60)
 		if setting.OAuthService.OAuthInfos[name].HostedDomain == "" {
 		if setting.OAuthService.OAuthInfos[name].HostedDomain == "" {
 			ctx.Redirect(connect.AuthCodeURL(state, oauth2.AccessTypeOnline))
 			ctx.Redirect(connect.AuthCodeURL(state, oauth2.AccessTypeOnline))
 		} else {
 		} else {
@@ -64,14 +69,20 @@ func OAuthLogin(ctx *m.ReqContext) {
 		return
 		return
 	}
 	}
 
 
-	savedState, ok := ctx.Session.Get(session.SESS_KEY_OAUTH_STATE).(string)
-	if !ok {
+	cookieState := ctx.GetCookie(OauthStateCookieName)
+
+	// delete cookie
+	ctx.Resp.Header().Del("Set-Cookie")
+	hs.deleteCookie(ctx.Resp, OauthStateCookieName)
+
+	if cookieState == "" {
 		ctx.Handle(500, "login.OAuthLogin(missing saved state)", nil)
 		ctx.Handle(500, "login.OAuthLogin(missing saved state)", nil)
 		return
 		return
 	}
 	}
 
 
-	queryState := ctx.Query("state")
-	if savedState != queryState {
+	queryState := hashStatecode(ctx.Query("state"), setting.OAuthService.OAuthInfos[name].ClientSecret)
+	oauthLogger.Info("state check", "queryState", queryState, "cookieState", cookieState)
+	if cookieState != queryState {
 		ctx.Handle(500, "login.OAuthLogin(state mismatch)", nil)
 		ctx.Handle(500, "login.OAuthLogin(state mismatch)", nil)
 		return
 		return
 	}
 	}
@@ -131,7 +142,7 @@ func OAuthLogin(ctx *m.ReqContext) {
 	userInfo, err := connect.UserInfo(client, token)
 	userInfo, err := connect.UserInfo(client, token)
 	if err != nil {
 	if err != nil {
 		if sErr, ok := err.(*social.Error); ok {
 		if sErr, ok := err.(*social.Error); ok {
-			redirectWithError(ctx, sErr)
+			hs.redirectWithError(ctx, sErr)
 		} else {
 		} else {
 			ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
 			ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
 		}
 		}
@@ -142,13 +153,13 @@ func OAuthLogin(ctx *m.ReqContext) {
 
 
 	// validate that we got at least an email address
 	// validate that we got at least an email address
 	if userInfo.Email == "" {
 	if userInfo.Email == "" {
-		redirectWithError(ctx, login.ErrNoEmail)
+		hs.redirectWithError(ctx, login.ErrNoEmail)
 		return
 		return
 	}
 	}
 
 
 	// validate that the email is allowed to login to grafana
 	// validate that the email is allowed to login to grafana
 	if !connect.IsEmailAllowed(userInfo.Email) {
 	if !connect.IsEmailAllowed(userInfo.Email) {
-		redirectWithError(ctx, login.ErrEmailNotAllowed)
+		hs.redirectWithError(ctx, login.ErrEmailNotAllowed)
 		return
 		return
 	}
 	}
 
 
@@ -171,14 +182,15 @@ func OAuthLogin(ctx *m.ReqContext) {
 		ExternalUser:  extUser,
 		ExternalUser:  extUser,
 		SignupAllowed: connect.IsSignupAllowed(),
 		SignupAllowed: connect.IsSignupAllowed(),
 	}
 	}
+
 	err = bus.Dispatch(cmd)
 	err = bus.Dispatch(cmd)
 	if err != nil {
 	if err != nil {
-		redirectWithError(ctx, err)
+		hs.redirectWithError(ctx, err)
 		return
 		return
 	}
 	}
 
 
 	// login
 	// login
-	loginUserWithUser(cmd.Result, ctx)
+	hs.loginUserWithUser(cmd.Result, ctx)
 
 
 	metrics.M_Api_Login_OAuth.Inc()
 	metrics.M_Api_Login_OAuth.Inc()
 
 
@@ -191,8 +203,29 @@ func OAuthLogin(ctx *m.ReqContext) {
 	ctx.Redirect(setting.AppSubUrl + "/")
 	ctx.Redirect(setting.AppSubUrl + "/")
 }
 }
 
 
-func redirectWithError(ctx *m.ReqContext, err error, v ...interface{}) {
+func (hs *HTTPServer) deleteCookie(w http.ResponseWriter, name string) {
+	hs.writeCookie(w, name, "", -1)
+}
+
+func (hs *HTTPServer) writeCookie(w http.ResponseWriter, name string, value string, maxAge int) {
+	http.SetCookie(w, &http.Cookie{
+		Name:     name,
+		MaxAge:   maxAge,
+		Value:    value,
+		HttpOnly: true,
+		Path:     setting.AppSubUrl + "/",
+		Secure:   hs.Cfg.SecurityHTTPSCookies,
+	})
+}
+
+func hashStatecode(code, seed string) string {
+	hashBytes := sha256.Sum256([]byte(code + setting.SecretKey + seed))
+	return hex.EncodeToString(hashBytes[:])
+}
+
+func (hs *HTTPServer) redirectWithError(ctx *m.ReqContext, err error, v ...interface{}) {
 	ctx.Logger.Error(err.Error(), v...)
 	ctx.Logger.Error(err.Error(), v...)
-	ctx.Session.Set("loginError", err.Error())
+	hs.trySetEncryptedCookie(ctx, LoginErrorCookieName, err.Error(), 60)
+
 	ctx.Redirect(setting.AppSubUrl + "/login")
 	ctx.Redirect(setting.AppSubUrl + "/login")
 }
 }

+ 2 - 2
pkg/api/org_invite.go

@@ -148,7 +148,7 @@ func GetInviteInfoByCode(c *m.ReqContext) Response {
 	})
 	})
 }
 }
 
 
-func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Response {
+func (hs *HTTPServer) CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Response {
 	query := m.GetTempUserByCodeQuery{Code: completeInvite.InviteCode}
 	query := m.GetTempUserByCodeQuery{Code: completeInvite.InviteCode}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
@@ -186,7 +186,7 @@ func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Res
 		return rsp
 		return rsp
 	}
 	}
 
 
-	loginUserWithUser(user, c)
+	hs.loginUserWithUser(user, c)
 
 
 	metrics.M_Api_User_SignUpCompleted.Inc()
 	metrics.M_Api_User_SignUpCompleted.Inc()
 	metrics.M_Api_User_SignUpInvite.Inc()
 	metrics.M_Api_User_SignUpInvite.Inc()

+ 2 - 2
pkg/api/signup.go

@@ -51,7 +51,7 @@ func SignUp(c *m.ReqContext, form dtos.SignUpForm) Response {
 	return JSON(200, util.DynMap{"status": "SignUpCreated"})
 	return JSON(200, util.DynMap{"status": "SignUpCreated"})
 }
 }
 
 
-func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
+func (hs *HTTPServer) SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
 	if !setting.AllowUserSignUp {
 	if !setting.AllowUserSignUp {
 		return Error(401, "User signup is disabled", nil)
 		return Error(401, "User signup is disabled", nil)
 	}
 	}
@@ -109,7 +109,7 @@ func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
 		apiResponse["code"] = "redirect-to-select-org"
 		apiResponse["code"] = "redirect-to-select-org"
 	}
 	}
 
 
-	loginUserWithUser(user, c)
+	hs.loginUserWithUser(user, c)
 	metrics.M_Api_User_SignUpCompleted.Inc()
 	metrics.M_Api_User_SignUpCompleted.Inc()
 
 
 	return JSON(200, apiResponse)
 	return JSON(200, apiResponse)

+ 0 - 11
pkg/middleware/auth.go

@@ -7,7 +7,6 @@ import (
 	"gopkg.in/macaron.v1"
 	"gopkg.in/macaron.v1"
 
 
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/grafana/grafana/pkg/util"
 )
 )
@@ -17,16 +16,6 @@ type AuthOptions struct {
 	ReqSignedIn     bool
 	ReqSignedIn     bool
 }
 }
 
 
-func getRequestUserId(c *m.ReqContext) int64 {
-	userID := c.Session.Get(session.SESS_KEY_USERID)
-
-	if userID != nil {
-		return userID.(int64)
-	}
-
-	return 0
-}
-
 func getApiKey(c *m.ReqContext) string {
 func getApiKey(c *m.ReqContext) string {
 	header := c.Req.Header.Get("Authorization")
 	header := c.Req.Header.Get("Authorization")
 	parts := strings.SplitN(header, " ", 2)
 	parts := strings.SplitN(header, " ", 2)

+ 19 - 1
pkg/middleware/auth_proxy.go

@@ -16,7 +16,9 @@ import (
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 )
 )
 
 
-var AUTH_PROXY_SESSION_VAR = "authProxyHeaderValue"
+var (
+	AUTH_PROXY_SESSION_VAR = "authProxyHeaderValue"
+)
 
 
 func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
 func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
 	if !setting.AuthProxyEnabled {
 	if !setting.AuthProxyEnabled {
@@ -40,6 +42,12 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
 		return false
 		return false
 	}
 	}
 
 
+	defer func() {
+		if err := ctx.Session.Release(); err != nil {
+			ctx.Logger.Error("failed to save session data", "error", err)
+		}
+	}()
+
 	query := &m.GetSignedInUserQuery{OrgId: orgID}
 	query := &m.GetSignedInUserQuery{OrgId: orgID}
 
 
 	// if this session has already been authenticated by authProxy just load the user
 	// if this session has already been authenticated by authProxy just load the user
@@ -192,6 +200,16 @@ var syncGrafanaUserWithLdapUser = func(query *m.LoginUserQuery) error {
 	return nil
 	return nil
 }
 }
 
 
+func getRequestUserId(c *m.ReqContext) int64 {
+	userID := c.Session.Get(session.SESS_KEY_USERID)
+
+	if userID != nil {
+		return userID.(int64)
+	}
+
+	return 0
+}
+
 func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error {
 func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error {
 	if len(strings.TrimSpace(setting.AuthProxyWhitelist)) == 0 {
 	if len(strings.TrimSpace(setting.AuthProxyWhitelist)) == 0 {
 		return nil
 		return nil

+ 5 - 28
pkg/middleware/middleware.go

@@ -3,15 +3,15 @@ package middleware
 import (
 import (
 	"strconv"
 	"strconv"
 
 
-	"gopkg.in/macaron.v1"
-
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/apikeygen"
 	"github.com/grafana/grafana/pkg/components/apikeygen"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/auth"
 	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/grafana/grafana/pkg/util"
+	macaron "gopkg.in/macaron.v1"
 )
 )
 
 
 var (
 var (
@@ -21,12 +21,12 @@ var (
 	ReqOrgAdmin     = RoleAuth(m.ROLE_ADMIN)
 	ReqOrgAdmin     = RoleAuth(m.ROLE_ADMIN)
 )
 )
 
 
-func GetContextHandler() macaron.Handler {
+func GetContextHandler(ats auth.UserAuthTokenService) macaron.Handler {
 	return func(c *macaron.Context) {
 	return func(c *macaron.Context) {
 		ctx := &m.ReqContext{
 		ctx := &m.ReqContext{
 			Context:        c,
 			Context:        c,
 			SignedInUser:   &m.SignedInUser{},
 			SignedInUser:   &m.SignedInUser{},
-			Session:        session.GetSession(),
+			Session:        session.GetSession(), // should only be used by auth_proxy
 			IsSignedIn:     false,
 			IsSignedIn:     false,
 			AllowAnonymous: false,
 			AllowAnonymous: false,
 			SkipCache:      false,
 			SkipCache:      false,
@@ -49,7 +49,7 @@ func GetContextHandler() macaron.Handler {
 		case initContextWithApiKey(ctx):
 		case initContextWithApiKey(ctx):
 		case initContextWithBasicAuth(ctx, orgId):
 		case initContextWithBasicAuth(ctx, orgId):
 		case initContextWithAuthProxy(ctx, orgId):
 		case initContextWithAuthProxy(ctx, orgId):
-		case initContextWithUserSessionCookie(ctx, orgId):
+		case ats.InitContextWithToken(ctx, orgId):
 		case initContextWithAnonymousUser(ctx):
 		case initContextWithAnonymousUser(ctx):
 		}
 		}
 
 
@@ -88,29 +88,6 @@ func initContextWithAnonymousUser(ctx *m.ReqContext) bool {
 	return true
 	return true
 }
 }
 
 
-func initContextWithUserSessionCookie(ctx *m.ReqContext, orgId int64) bool {
-	// initialize session
-	if err := ctx.Session.Start(ctx.Context); err != nil {
-		ctx.Logger.Error("Failed to start session", "error", err)
-		return false
-	}
-
-	var userId int64
-	if userId = getRequestUserId(ctx); userId == 0 {
-		return false
-	}
-
-	query := m.GetSignedInUserQuery{UserId: userId, OrgId: orgId}
-	if err := bus.Dispatch(&query); err != nil {
-		ctx.Logger.Error("Failed to get user with id", "userId", userId, "error", err)
-		return false
-	}
-
-	ctx.SignedInUser = query.Result
-	ctx.IsSignedIn = true
-	return true
-}
-
 func initContextWithApiKey(ctx *m.ReqContext) bool {
 func initContextWithApiKey(ctx *m.ReqContext) bool {
 	var keyString string
 	var keyString string
 	if keyString = getApiKey(ctx); keyString == "" {
 	if keyString = getApiKey(ctx); keyString == "" {

+ 48 - 30
pkg/middleware/middleware_test.go

@@ -7,7 +7,7 @@ import (
 	"path/filepath"
 	"path/filepath"
 	"testing"
 	"testing"
 
 
-	ms "github.com/go-macaron/session"
+	msession "github.com/go-macaron/session"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/services/session"
@@ -43,11 +43,6 @@ func TestMiddlewareContext(t *testing.T) {
 			So(sc.resp.Header().Get("Cache-Control"), ShouldBeEmpty)
 			So(sc.resp.Header().Get("Cache-Control"), ShouldBeEmpty)
 		})
 		})
 
 
-		middlewareScenario("Non api request should init session", func(sc *scenarioContext) {
-			sc.fakeReq("GET", "/").exec()
-			So(sc.resp.Header().Get("Set-Cookie"), ShouldContainSubstring, "grafana_sess")
-		})
-
 		middlewareScenario("Invalid api key", func(sc *scenarioContext) {
 		middlewareScenario("Invalid api key", func(sc *scenarioContext) {
 			sc.apiKey = "invalid_key_test"
 			sc.apiKey = "invalid_key_test"
 			sc.fakeReq("GET", "/").exec()
 			sc.fakeReq("GET", "/").exec()
@@ -151,22 +146,17 @@ func TestMiddlewareContext(t *testing.T) {
 			})
 			})
 		})
 		})
 
 
-		middlewareScenario("UserId in session", func(sc *scenarioContext) {
-
-			sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
-				c.Session.Set(session.SESS_KEY_USERID, int64(12))
-			}).exec()
-
-			bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
-				query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
-				return nil
-			})
+		middlewareScenario("Auth token service", func(sc *scenarioContext) {
+			var wasCalled bool
+			sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool {
+				wasCalled = true
+				return false
+			}
 
 
 			sc.fakeReq("GET", "/").exec()
 			sc.fakeReq("GET", "/").exec()
 
 
-			Convey("should init context with user info", func() {
-				So(sc.context.IsSignedIn, ShouldBeTrue)
-				So(sc.context.UserId, ShouldEqual, 12)
+			Convey("should call middleware", func() {
+				So(wasCalled, ShouldBeTrue)
 			})
 			})
 		})
 		})
 
 
@@ -211,6 +201,7 @@ func TestMiddlewareContext(t *testing.T) {
 				return nil
 				return nil
 			})
 			})
 
 
+			setting.SessionOptions = msession.Options{}
 			sc.fakeReq("GET", "/")
 			sc.fakeReq("GET", "/")
 			sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
 			sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
 			sc.exec()
 			sc.exec()
@@ -479,6 +470,7 @@ func middlewareScenario(desc string, fn scenarioFunc) {
 		defer bus.ClearBusHandlers()
 		defer bus.ClearBusHandlers()
 
 
 		sc := &scenarioContext{}
 		sc := &scenarioContext{}
+
 		viewsPath, _ := filepath.Abs("../../public/views")
 		viewsPath, _ := filepath.Abs("../../public/views")
 
 
 		sc.m = macaron.New()
 		sc.m = macaron.New()
@@ -487,10 +479,13 @@ func middlewareScenario(desc string, fn scenarioFunc) {
 			Delims:    macaron.Delims{Left: "[[", Right: "]]"},
 			Delims:    macaron.Delims{Left: "[[", Right: "]]"},
 		}))
 		}))
 
 
-		sc.m.Use(GetContextHandler())
+		session.Init(&msession.Options{}, 0)
+		sc.userAuthTokenService = newFakeUserAuthTokenService()
+		sc.m.Use(GetContextHandler(sc.userAuthTokenService))
 		// mock out gc goroutine
 		// mock out gc goroutine
 		session.StartSessionGC = func() {}
 		session.StartSessionGC = func() {}
-		sc.m.Use(Sessioner(&ms.Options{}, 0))
+		setting.SessionOptions = msession.Options{}
+
 		sc.m.Use(OrgRedirect())
 		sc.m.Use(OrgRedirect())
 		sc.m.Use(AddDefaultResponseHeaders())
 		sc.m.Use(AddDefaultResponseHeaders())
 
 
@@ -508,15 +503,16 @@ func middlewareScenario(desc string, fn scenarioFunc) {
 }
 }
 
 
 type scenarioContext struct {
 type scenarioContext struct {
-	m              *macaron.Macaron
-	context        *m.ReqContext
-	resp           *httptest.ResponseRecorder
-	apiKey         string
-	authHeader     string
-	respJson       map[string]interface{}
-	handlerFunc    handlerFunc
-	defaultHandler macaron.Handler
-	url            string
+	m                    *macaron.Macaron
+	context              *m.ReqContext
+	resp                 *httptest.ResponseRecorder
+	apiKey               string
+	authHeader           string
+	respJson             map[string]interface{}
+	handlerFunc          handlerFunc
+	defaultHandler       macaron.Handler
+	url                  string
+	userAuthTokenService *fakeUserAuthTokenService
 
 
 	req *http.Request
 	req *http.Request
 }
 }
@@ -585,3 +581,25 @@ func (sc *scenarioContext) exec() {
 
 
 type scenarioFunc func(c *scenarioContext)
 type scenarioFunc func(c *scenarioContext)
 type handlerFunc func(c *m.ReqContext)
 type handlerFunc func(c *m.ReqContext)
+
+type fakeUserAuthTokenService struct {
+	initContextWithTokenProvider func(ctx *m.ReqContext, orgID int64) bool
+}
+
+func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
+	return &fakeUserAuthTokenService{
+		initContextWithTokenProvider: func(ctx *m.ReqContext, orgID int64) bool {
+			return false
+		},
+	}
+}
+
+func (s *fakeUserAuthTokenService) InitContextWithToken(ctx *m.ReqContext, orgID int64) bool {
+	return s.initContextWithTokenProvider(ctx, orgID)
+}
+
+func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqContext) error {
+	return nil
+}
+
+func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) {}

+ 0 - 1
pkg/middleware/org_redirect.go

@@ -9,7 +9,6 @@ import (
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
-
 	"gopkg.in/macaron.v1"
 	"gopkg.in/macaron.v1"
 )
 )
 
 

+ 11 - 13
pkg/middleware/org_redirect_test.go

@@ -7,7 +7,6 @@ import (
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/session"
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
 )
 )
 
 
@@ -15,18 +14,15 @@ func TestOrgRedirectMiddleware(t *testing.T) {
 
 
 	Convey("Can redirect to correct org", t, func() {
 	Convey("Can redirect to correct org", t, func() {
 		middlewareScenario("when setting a correct org for the user", func(sc *scenarioContext) {
 		middlewareScenario("when setting a correct org for the user", func(sc *scenarioContext) {
-			sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
-				c.Session.Set(session.SESS_KEY_USERID, int64(12))
-			}).exec()
-
 			bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error {
 			bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error {
 				return nil
 				return nil
 			})
 			})
 
 
-			bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
-				query.Result = &m.SignedInUser{OrgId: 1, UserId: 12}
-				return nil
-			})
+			sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool {
+				ctx.SignedInUser = &m.SignedInUser{OrgId: 1, UserId: 12}
+				ctx.IsSignedIn = true
+				return true
+			}
 
 
 			sc.m.Get("/", sc.defaultHandler)
 			sc.m.Get("/", sc.defaultHandler)
 			sc.fakeReq("GET", "/?orgId=3").exec()
 			sc.fakeReq("GET", "/?orgId=3").exec()
@@ -37,14 +33,16 @@ func TestOrgRedirectMiddleware(t *testing.T) {
 		})
 		})
 
 
 		middlewareScenario("when setting an invalid org for user", func(sc *scenarioContext) {
 		middlewareScenario("when setting an invalid org for user", func(sc *scenarioContext) {
-			sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
-				c.Session.Set(session.SESS_KEY_USERID, int64(12))
-			}).exec()
-
 			bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error {
 			bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error {
 				return fmt.Errorf("")
 				return fmt.Errorf("")
 			})
 			})
 
 
+			sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool {
+				ctx.SignedInUser = &m.SignedInUser{OrgId: 1, UserId: 12}
+				ctx.IsSignedIn = true
+				return true
+			}
+
 			bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
 			bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
 				query.Result = &m.SignedInUser{OrgId: 1, UserId: 12}
 				query.Result = &m.SignedInUser{OrgId: 1, UserId: 12}
 				return nil
 				return nil

+ 5 - 8
pkg/middleware/quota_test.go

@@ -74,15 +74,12 @@ func TestMiddlewareQuota(t *testing.T) {
 		})
 		})
 
 
 		middlewareScenario("with user logged in", func(sc *scenarioContext) {
 		middlewareScenario("with user logged in", func(sc *scenarioContext) {
-			// log us in, so we have a user_id and org_id in the context
-			sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
-				c.Session.Set(session.SESS_KEY_USERID, int64(12))
-			}).exec()
+			sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool {
+				ctx.SignedInUser = &m.SignedInUser{OrgId: 2, UserId: 12}
+				ctx.IsSignedIn = true
+				return true
+			}
 
 
-			bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
-				query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
-				return nil
-			})
 			bus.AddHandler("globalQuota", func(query *m.GetGlobalQuotaByTargetQuery) error {
 			bus.AddHandler("globalQuota", func(query *m.GetGlobalQuotaByTargetQuery) error {
 				query.Result = &m.GlobalQuotaDTO{
 				query.Result = &m.GlobalQuotaDTO{
 					Target: query.Target,
 					Target: query.Target,

+ 3 - 4
pkg/middleware/recovery_test.go

@@ -4,13 +4,12 @@ import (
 	"path/filepath"
 	"path/filepath"
 	"testing"
 	"testing"
 
 
-	ms "github.com/go-macaron/session"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
-	"gopkg.in/macaron.v1"
+	macaron "gopkg.in/macaron.v1"
 )
 )
 
 
 func TestRecoveryMiddleware(t *testing.T) {
 func TestRecoveryMiddleware(t *testing.T) {
@@ -64,10 +63,10 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) {
 			Delims:    macaron.Delims{Left: "[[", Right: "]]"},
 			Delims:    macaron.Delims{Left: "[[", Right: "]]"},
 		}))
 		}))
 
 
-		sc.m.Use(GetContextHandler())
+		sc.userAuthTokenService = newFakeUserAuthTokenService()
+		sc.m.Use(GetContextHandler(sc.userAuthTokenService))
 		// mock out gc goroutine
 		// mock out gc goroutine
 		session.StartSessionGC = func() {}
 		session.StartSessionGC = func() {}
-		sc.m.Use(Sessioner(&ms.Options{}, 0))
 		sc.m.Use(OrgRedirect())
 		sc.m.Use(OrgRedirect())
 		sc.m.Use(AddDefaultResponseHeaders())
 		sc.m.Use(AddDefaultResponseHeaders())
 
 

+ 0 - 21
pkg/middleware/session.go

@@ -1,21 +0,0 @@
-package middleware
-
-import (
-	ms "github.com/go-macaron/session"
-	"gopkg.in/macaron.v1"
-
-	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/session"
-)
-
-func Sessioner(options *ms.Options, sessionConnMaxLifetime int64) macaron.Handler {
-	session.Init(options, sessionConnMaxLifetime)
-
-	return func(ctx *m.ReqContext) {
-		ctx.Next()
-
-		if err := ctx.Session.Release(); err != nil {
-			panic("session(release): " + err.Error())
-		}
-	}
-}

+ 3 - 3
pkg/models/context.go

@@ -3,18 +3,18 @@ package models
 import (
 import (
 	"strings"
 	"strings"
 
 
-	"github.com/prometheus/client_golang/prometheus"
-	"gopkg.in/macaron.v1"
-
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
+	"github.com/prometheus/client_golang/prometheus"
+	"gopkg.in/macaron.v1"
 )
 )
 
 
 type ReqContext struct {
 type ReqContext struct {
 	*macaron.Context
 	*macaron.Context
 	*SignedInUser
 	*SignedInUser
 
 
+	// This should only be used by the auth_proxy
 	Session session.SessionStore
 	Session session.SessionStore
 
 
 	IsSignedIn     bool
 	IsSignedIn     bool

+ 266 - 0
pkg/services/auth/auth_token.go

@@ -0,0 +1,266 @@
+package auth
+
+import (
+	"crypto/sha256"
+	"encoding/hex"
+	"net/http"
+	"net/url"
+	"time"
+
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/infra/serverlock"
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/registry"
+	"github.com/grafana/grafana/pkg/services/sqlstore"
+	"github.com/grafana/grafana/pkg/setting"
+	"github.com/grafana/grafana/pkg/util"
+)
+
+func init() {
+	registry.RegisterService(&UserAuthTokenServiceImpl{})
+}
+
+var (
+	getTime          = time.Now
+	UrgentRotateTime = 1 * time.Minute
+	oneYearInSeconds = 31557600 //used as default maxage for session cookies. We validate/rotate them more often.
+)
+
+// UserAuthTokenService are used for generating and validating user auth tokens
+type UserAuthTokenService interface {
+	InitContextWithToken(ctx *models.ReqContext, orgID int64) bool
+	UserAuthenticatedHook(user *models.User, c *models.ReqContext) error
+	UserSignedOutHook(c *models.ReqContext)
+}
+
+type UserAuthTokenServiceImpl struct {
+	SQLStore          *sqlstore.SqlStore            `inject:""`
+	ServerLockService *serverlock.ServerLockService `inject:""`
+	Cfg               *setting.Cfg                  `inject:""`
+	log               log.Logger
+}
+
+// Init this service
+func (s *UserAuthTokenServiceImpl) Init() error {
+	s.log = log.New("auth")
+	return nil
+}
+
+func (s *UserAuthTokenServiceImpl) InitContextWithToken(ctx *models.ReqContext, orgID int64) bool {
+	//auth User
+	unhashedToken := ctx.GetCookie(s.Cfg.LoginCookieName)
+	if unhashedToken == "" {
+		return false
+	}
+
+	userToken, err := s.LookupToken(unhashedToken)
+	if err != nil {
+		ctx.Logger.Info("failed to look up user based on cookie", "error", err)
+		return false
+	}
+
+	query := models.GetSignedInUserQuery{UserId: userToken.UserId, OrgId: orgID}
+	if err := bus.Dispatch(&query); err != nil {
+		ctx.Logger.Error("Failed to get user with id", "userId", userToken.UserId, "error", err)
+		return false
+	}
+
+	ctx.SignedInUser = query.Result
+	ctx.IsSignedIn = true
+
+	//rotate session token if needed.
+	rotated, err := s.RefreshToken(userToken, ctx.RemoteAddr(), ctx.Req.UserAgent())
+	if err != nil {
+		ctx.Logger.Error("failed to rotate token", "error", err, "userId", userToken.UserId, "tokenId", userToken.Id)
+		return true
+	}
+
+	if rotated {
+		s.writeSessionCookie(ctx, userToken.UnhashedToken, oneYearInSeconds)
+	}
+
+	return true
+}
+
+func (s *UserAuthTokenServiceImpl) writeSessionCookie(ctx *models.ReqContext, value string, maxAge int) {
+	if setting.Env == setting.DEV {
+		ctx.Logger.Info("new token", "unhashed token", value)
+	}
+
+	ctx.Resp.Header().Del("Set-Cookie")
+	cookie := http.Cookie{
+		Name:     s.Cfg.LoginCookieName,
+		Value:    url.QueryEscape(value),
+		HttpOnly: true,
+		Path:     setting.AppSubUrl + "/",
+		Secure:   s.Cfg.SecurityHTTPSCookies,
+		MaxAge:   maxAge,
+	}
+
+	http.SetCookie(ctx.Resp, &cookie)
+}
+
+func (s *UserAuthTokenServiceImpl) UserAuthenticatedHook(user *models.User, c *models.ReqContext) error {
+	userToken, err := s.CreateToken(user.Id, c.RemoteAddr(), c.Req.UserAgent())
+	if err != nil {
+		return err
+	}
+
+	s.writeSessionCookie(c, userToken.UnhashedToken, oneYearInSeconds)
+	return nil
+}
+
+func (s *UserAuthTokenServiceImpl) UserSignedOutHook(c *models.ReqContext) {
+	s.writeSessionCookie(c, "", -1)
+}
+
+func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent string) (*userAuthToken, error) {
+	clientIP = util.ParseIPAddress(clientIP)
+	token, err := util.RandomHex(16)
+	if err != nil {
+		return nil, err
+	}
+
+	hashedToken := hashToken(token)
+
+	now := getTime().Unix()
+
+	userToken := userAuthToken{
+		UserId:        userId,
+		AuthToken:     hashedToken,
+		PrevAuthToken: hashedToken,
+		ClientIp:      clientIP,
+		UserAgent:     userAgent,
+		RotatedAt:     now,
+		CreatedAt:     now,
+		UpdatedAt:     now,
+		SeenAt:        0,
+		AuthTokenSeen: false,
+	}
+	_, err = s.SQLStore.NewSession().Insert(&userToken)
+	if err != nil {
+		return nil, err
+	}
+
+	userToken.UnhashedToken = token
+
+	return &userToken, nil
+}
+
+func (s *UserAuthTokenServiceImpl) LookupToken(unhashedToken string) (*userAuthToken, error) {
+	hashedToken := hashToken(unhashedToken)
+	if setting.Env == setting.DEV {
+		s.log.Info("looking up token", "unhashed", unhashedToken, "hashed", hashedToken)
+	}
+
+	expireBefore := getTime().Add(time.Duration(-86400*s.Cfg.LoginCookieMaxDays) * time.Second).Unix()
+
+	var userToken userAuthToken
+	exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ?", hashedToken, hashedToken, expireBefore).Get(&userToken)
+	if err != nil {
+		return nil, err
+	}
+
+	if !exists {
+		return nil, ErrAuthTokenNotFound
+	}
+
+	if userToken.AuthToken != hashedToken && userToken.PrevAuthToken == hashedToken && userToken.AuthTokenSeen {
+		userTokenCopy := userToken
+		userTokenCopy.AuthTokenSeen = false
+		expireBefore := getTime().Add(-UrgentRotateTime).Unix()
+		affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND prev_auth_token = ? AND rotated_at < ?", userTokenCopy.Id, userTokenCopy.PrevAuthToken, expireBefore).AllCols().Update(&userTokenCopy)
+		if err != nil {
+			return nil, err
+		}
+
+		if affectedRows == 0 {
+			s.log.Debug("prev seen token unchanged", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
+		} else {
+			s.log.Debug("prev seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
+		}
+	}
+
+	if !userToken.AuthTokenSeen && userToken.AuthToken == hashedToken {
+		userTokenCopy := userToken
+		userTokenCopy.AuthTokenSeen = true
+		userTokenCopy.SeenAt = getTime().Unix()
+		affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND auth_token = ?", userTokenCopy.Id, userTokenCopy.AuthToken).AllCols().Update(&userTokenCopy)
+		if err != nil {
+			return nil, err
+		}
+
+		if affectedRows == 1 {
+			userToken = userTokenCopy
+		}
+
+		if affectedRows == 0 {
+			s.log.Debug("seen wrong token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
+		} else {
+			s.log.Debug("seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
+		}
+	}
+
+	userToken.UnhashedToken = unhashedToken
+
+	return &userToken, nil
+}
+
+func (s *UserAuthTokenServiceImpl) RefreshToken(token *userAuthToken, clientIP, userAgent string) (bool, error) {
+	if token == nil {
+		return false, nil
+	}
+
+	now := getTime()
+
+	needsRotation := false
+	rotatedAt := time.Unix(token.RotatedAt, 0)
+	if token.AuthTokenSeen {
+		needsRotation = rotatedAt.Before(now.Add(-time.Duration(s.Cfg.LoginCookieRotation) * time.Minute))
+	} else {
+		needsRotation = rotatedAt.Before(now.Add(-UrgentRotateTime))
+	}
+
+	if !needsRotation {
+		return false, nil
+	}
+
+	s.log.Debug("refresh token needs rotation?", "auth_token_seen", token.AuthTokenSeen, "rotated_at", rotatedAt, "token.Id", token.Id)
+
+	clientIP = util.ParseIPAddress(clientIP)
+	newToken, _ := util.RandomHex(16)
+	hashedToken := hashToken(newToken)
+
+	// very important that auth_token_seen is set after the prev_auth_token = case when ... for mysql to function correctly
+	sql := `
+		UPDATE user_auth_token
+		SET
+			seen_at = 0,
+			user_agent = ?,
+			client_ip = ?,
+			prev_auth_token = case when auth_token_seen = ? then auth_token else prev_auth_token end,
+			auth_token = ?,
+			auth_token_seen = ?,
+			rotated_at = ?
+		WHERE id = ? AND (auth_token_seen = ? OR rotated_at < ?)`
+
+	res, err := s.SQLStore.NewSession().Exec(sql, userAgent, clientIP, s.SQLStore.Dialect.BooleanStr(true), hashedToken, s.SQLStore.Dialect.BooleanStr(false), now.Unix(), token.Id, s.SQLStore.Dialect.BooleanStr(true), now.Add(-30*time.Second).Unix())
+	if err != nil {
+		return false, err
+	}
+
+	affected, _ := res.RowsAffected()
+	s.log.Debug("rotated", "affected", affected, "auth_token_id", token.Id, "userId", token.UserId)
+	if affected > 0 {
+		token.UnhashedToken = newToken
+		return true, nil
+	}
+
+	return false, nil
+}
+
+func hashToken(token string) string {
+	hashBytes := sha256.Sum256([]byte(token + setting.SecretKey))
+	return hex.EncodeToString(hashBytes[:])
+}

+ 339 - 0
pkg/services/auth/auth_token_test.go

@@ -0,0 +1,339 @@
+package auth
+
+import (
+	"testing"
+	"time"
+
+	"github.com/grafana/grafana/pkg/setting"
+
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/services/sqlstore"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestUserAuthToken(t *testing.T) {
+	Convey("Test user auth token", t, func() {
+		ctx := createTestContext(t)
+		userAuthTokenService := ctx.tokenService
+		userID := int64(10)
+
+		t := time.Date(2018, 12, 13, 13, 45, 0, 0, time.UTC)
+		getTime = func() time.Time {
+			return t
+		}
+
+		Convey("When creating token", func() {
+			token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
+			So(err, ShouldBeNil)
+			So(token, ShouldNotBeNil)
+			So(token.AuthTokenSeen, ShouldBeFalse)
+
+			Convey("When lookup unhashed token should return user auth token", func() {
+				LookupToken, err := userAuthTokenService.LookupToken(token.UnhashedToken)
+				So(err, ShouldBeNil)
+				So(LookupToken, ShouldNotBeNil)
+				So(LookupToken.UserId, ShouldEqual, userID)
+				So(LookupToken.AuthTokenSeen, ShouldBeTrue)
+
+				storedAuthToken, err := ctx.getAuthTokenByID(LookupToken.Id)
+				So(err, ShouldBeNil)
+				So(storedAuthToken, ShouldNotBeNil)
+				So(storedAuthToken.AuthTokenSeen, ShouldBeTrue)
+			})
+
+			Convey("When lookup hashed token should return user auth token not found error", func() {
+				LookupToken, err := userAuthTokenService.LookupToken(token.AuthToken)
+				So(err, ShouldEqual, ErrAuthTokenNotFound)
+				So(LookupToken, ShouldBeNil)
+			})
+		})
+
+		Convey("expires correctly", func() {
+			token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
+			So(err, ShouldBeNil)
+			So(token, ShouldNotBeNil)
+
+			_, err = userAuthTokenService.LookupToken(token.UnhashedToken)
+			So(err, ShouldBeNil)
+
+			token, err = ctx.getAuthTokenByID(token.Id)
+			So(err, ShouldBeNil)
+
+			getTime = func() time.Time {
+				return t.Add(time.Hour)
+			}
+
+			refreshed, err := userAuthTokenService.RefreshToken(token, "192.168.10.11:1234", "some user agent")
+			So(err, ShouldBeNil)
+			So(refreshed, ShouldBeTrue)
+
+			_, err = userAuthTokenService.LookupToken(token.UnhashedToken)
+			So(err, ShouldBeNil)
+
+			stillGood, err := userAuthTokenService.LookupToken(token.UnhashedToken)
+			So(err, ShouldBeNil)
+			So(stillGood, ShouldNotBeNil)
+
+			getTime = func() time.Time {
+				return t.Add(24 * 7 * time.Hour)
+			}
+			notGood, err := userAuthTokenService.LookupToken(token.UnhashedToken)
+			So(err, ShouldEqual, ErrAuthTokenNotFound)
+			So(notGood, ShouldBeNil)
+		})
+
+		Convey("can properly rotate tokens", func() {
+			token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
+			So(err, ShouldBeNil)
+			So(token, ShouldNotBeNil)
+
+			prevToken := token.AuthToken
+			unhashedPrev := token.UnhashedToken
+
+			refreshed, err := userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent")
+			So(err, ShouldBeNil)
+			So(refreshed, ShouldBeFalse)
+
+			updated, err := ctx.markAuthTokenAsSeen(token.Id)
+			So(err, ShouldBeNil)
+			So(updated, ShouldBeTrue)
+
+			token, err = ctx.getAuthTokenByID(token.Id)
+			So(err, ShouldBeNil)
+
+			getTime = func() time.Time {
+				return t.Add(time.Hour)
+			}
+
+			refreshed, err = userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent")
+			So(err, ShouldBeNil)
+			So(refreshed, ShouldBeTrue)
+
+			unhashedToken := token.UnhashedToken
+
+			token, err = ctx.getAuthTokenByID(token.Id)
+			So(err, ShouldBeNil)
+			token.UnhashedToken = unhashedToken
+
+			So(token.RotatedAt, ShouldEqual, getTime().Unix())
+			So(token.ClientIp, ShouldEqual, "192.168.10.12")
+			So(token.UserAgent, ShouldEqual, "a new user agent")
+			So(token.AuthTokenSeen, ShouldBeFalse)
+			So(token.SeenAt, ShouldEqual, 0)
+			So(token.PrevAuthToken, ShouldEqual, prevToken)
+
+			// ability to auth using an old token
+
+			lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken)
+			So(err, ShouldBeNil)
+			So(lookedUp, ShouldNotBeNil)
+			So(lookedUp.AuthTokenSeen, ShouldBeTrue)
+			So(lookedUp.SeenAt, ShouldEqual, getTime().Unix())
+
+			lookedUp, err = userAuthTokenService.LookupToken(unhashedPrev)
+			So(err, ShouldBeNil)
+			So(lookedUp, ShouldNotBeNil)
+			So(lookedUp.Id, ShouldEqual, token.Id)
+			So(lookedUp.AuthTokenSeen, ShouldBeTrue)
+
+			getTime = func() time.Time {
+				return t.Add(time.Hour + (2 * time.Minute))
+			}
+
+			lookedUp, err = userAuthTokenService.LookupToken(unhashedPrev)
+			So(err, ShouldBeNil)
+			So(lookedUp, ShouldNotBeNil)
+			So(lookedUp.AuthTokenSeen, ShouldBeTrue)
+
+			lookedUp, err = ctx.getAuthTokenByID(lookedUp.Id)
+			So(err, ShouldBeNil)
+			So(lookedUp, ShouldNotBeNil)
+			So(lookedUp.AuthTokenSeen, ShouldBeFalse)
+
+			refreshed, err = userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent")
+			So(err, ShouldBeNil)
+			So(refreshed, ShouldBeTrue)
+
+			token, err = ctx.getAuthTokenByID(token.Id)
+			So(err, ShouldBeNil)
+			So(token, ShouldNotBeNil)
+			So(token.SeenAt, ShouldEqual, 0)
+		})
+
+		Convey("keeps prev token valid for 1 minute after it is confirmed", func() {
+			token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
+			So(err, ShouldBeNil)
+			So(token, ShouldNotBeNil)
+
+			lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken)
+			So(err, ShouldBeNil)
+			So(lookedUp, ShouldNotBeNil)
+
+			getTime = func() time.Time {
+				return t.Add(10 * time.Minute)
+			}
+
+			prevToken := token.UnhashedToken
+			refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox")
+			So(err, ShouldBeNil)
+			So(refreshed, ShouldBeTrue)
+
+			getTime = func() time.Time {
+				return t.Add(20 * time.Minute)
+			}
+
+			current, err := userAuthTokenService.LookupToken(token.UnhashedToken)
+			So(err, ShouldBeNil)
+			So(current, ShouldNotBeNil)
+
+			prev, err := userAuthTokenService.LookupToken(prevToken)
+			So(err, ShouldBeNil)
+			So(prev, ShouldNotBeNil)
+		})
+
+		Convey("will not mark token unseen when prev and current are the same", func() {
+			token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
+			So(err, ShouldBeNil)
+			So(token, ShouldNotBeNil)
+
+			lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken)
+			So(err, ShouldBeNil)
+			So(lookedUp, ShouldNotBeNil)
+
+			lookedUp, err = userAuthTokenService.LookupToken(token.UnhashedToken)
+			So(err, ShouldBeNil)
+			So(lookedUp, ShouldNotBeNil)
+
+			lookedUp, err = ctx.getAuthTokenByID(lookedUp.Id)
+			So(err, ShouldBeNil)
+			So(lookedUp, ShouldNotBeNil)
+			So(lookedUp.AuthTokenSeen, ShouldBeTrue)
+		})
+
+		Convey("Rotate token", func() {
+			token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
+			So(err, ShouldBeNil)
+			So(token, ShouldNotBeNil)
+
+			prevToken := token.AuthToken
+
+			Convey("Should rotate current token and previous token when auth token seen", func() {
+				updated, err := ctx.markAuthTokenAsSeen(token.Id)
+				So(err, ShouldBeNil)
+				So(updated, ShouldBeTrue)
+
+				getTime = func() time.Time {
+					return t.Add(10 * time.Minute)
+				}
+
+				refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox")
+				So(err, ShouldBeNil)
+				So(refreshed, ShouldBeTrue)
+
+				storedToken, err := ctx.getAuthTokenByID(token.Id)
+				So(err, ShouldBeNil)
+				So(storedToken, ShouldNotBeNil)
+				So(storedToken.AuthTokenSeen, ShouldBeFalse)
+				So(storedToken.PrevAuthToken, ShouldEqual, prevToken)
+				So(storedToken.AuthToken, ShouldNotEqual, prevToken)
+
+				prevToken = storedToken.AuthToken
+
+				updated, err = ctx.markAuthTokenAsSeen(token.Id)
+				So(err, ShouldBeNil)
+				So(updated, ShouldBeTrue)
+
+				getTime = func() time.Time {
+					return t.Add(20 * time.Minute)
+				}
+
+				refreshed, err = userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox")
+				So(err, ShouldBeNil)
+				So(refreshed, ShouldBeTrue)
+
+				storedToken, err = ctx.getAuthTokenByID(token.Id)
+				So(err, ShouldBeNil)
+				So(storedToken, ShouldNotBeNil)
+				So(storedToken.AuthTokenSeen, ShouldBeFalse)
+				So(storedToken.PrevAuthToken, ShouldEqual, prevToken)
+				So(storedToken.AuthToken, ShouldNotEqual, prevToken)
+			})
+
+			Convey("Should rotate current token, but keep previous token when auth token not seen", func() {
+				token.RotatedAt = getTime().Add(-2 * time.Minute).Unix()
+
+				getTime = func() time.Time {
+					return t.Add(2 * time.Minute)
+				}
+
+				refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox")
+				So(err, ShouldBeNil)
+				So(refreshed, ShouldBeTrue)
+
+				storedToken, err := ctx.getAuthTokenByID(token.Id)
+				So(err, ShouldBeNil)
+				So(storedToken, ShouldNotBeNil)
+				So(storedToken.AuthTokenSeen, ShouldBeFalse)
+				So(storedToken.PrevAuthToken, ShouldEqual, prevToken)
+				So(storedToken.AuthToken, ShouldNotEqual, prevToken)
+			})
+		})
+
+		Reset(func() {
+			getTime = time.Now
+		})
+	})
+}
+
+func createTestContext(t *testing.T) *testContext {
+	t.Helper()
+
+	sqlstore := sqlstore.InitTestDB(t)
+	tokenService := &UserAuthTokenServiceImpl{
+		SQLStore: sqlstore,
+		Cfg: &setting.Cfg{
+			LoginCookieName:                   "grafana_session",
+			LoginCookieMaxDays:                7,
+			LoginDeleteExpiredTokensAfterDays: 30,
+			LoginCookieRotation:               10,
+		},
+		log: log.New("test-logger"),
+	}
+
+	UrgentRotateTime = time.Minute
+
+	return &testContext{
+		sqlstore:     sqlstore,
+		tokenService: tokenService,
+	}
+}
+
+type testContext struct {
+	sqlstore     *sqlstore.SqlStore
+	tokenService *UserAuthTokenServiceImpl
+}
+
+func (c *testContext) getAuthTokenByID(id int64) (*userAuthToken, error) {
+	sess := c.sqlstore.NewSession()
+	var t userAuthToken
+	found, err := sess.ID(id).Get(&t)
+	if err != nil || !found {
+		return nil, err
+	}
+
+	return &t, nil
+}
+
+func (c *testContext) markAuthTokenAsSeen(id int64) (bool, error) {
+	sess := c.sqlstore.NewSession()
+	res, err := sess.Exec("UPDATE user_auth_token SET auth_token_seen = ? WHERE id = ?", c.sqlstore.Dialect.BooleanStr(true), id)
+	if err != nil {
+		return false, err
+	}
+
+	rowsAffected, err := res.RowsAffected()
+	if err != nil {
+		return false, err
+	}
+	return rowsAffected == 1, nil
+}

+ 25 - 0
pkg/services/auth/model.go

@@ -0,0 +1,25 @@
+package auth
+
+import (
+	"errors"
+)
+
+// Typed errors
+var (
+	ErrAuthTokenNotFound = errors.New("User auth token not found")
+)
+
+type userAuthToken struct {
+	Id            int64
+	UserId        int64
+	AuthToken     string
+	PrevAuthToken string
+	UserAgent     string
+	ClientIp      string
+	AuthTokenSeen bool
+	SeenAt        int64
+	RotatedAt     int64
+	CreatedAt     int64
+	UpdatedAt     int64
+	UnhashedToken string `xorm:"-"`
+}

+ 38 - 0
pkg/services/auth/session_cleanup.go

@@ -0,0 +1,38 @@
+package auth
+
+import (
+	"context"
+	"time"
+)
+
+func (srv *UserAuthTokenServiceImpl) Run(ctx context.Context) error {
+	ticker := time.NewTicker(time.Hour * 12)
+	deleteSessionAfter := time.Hour * 24 * time.Duration(srv.Cfg.LoginDeleteExpiredTokensAfterDays)
+
+	for {
+		select {
+		case <-ticker.C:
+			srv.ServerLockService.LockAndExecute(ctx, "delete old sessions", time.Hour*12, func() {
+				srv.deleteOldSession(deleteSessionAfter)
+			})
+
+		case <-ctx.Done():
+			return ctx.Err()
+		}
+	}
+}
+
+func (srv *UserAuthTokenServiceImpl) deleteOldSession(deleteSessionAfter time.Duration) (int64, error) {
+	sql := `DELETE from user_auth_token WHERE rotated_at < ?`
+
+	deleteBefore := getTime().Add(-deleteSessionAfter)
+	res, err := srv.SQLStore.NewSession().Exec(sql, deleteBefore.Unix())
+	if err != nil {
+		return 0, err
+	}
+
+	affected, err := res.RowsAffected()
+	srv.log.Info("deleted old sessions", "count", affected)
+
+	return affected, err
+}

+ 36 - 0
pkg/services/auth/session_cleanup_test.go

@@ -0,0 +1,36 @@
+package auth
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestUserAuthTokenCleanup(t *testing.T) {
+
+	Convey("Test user auth token cleanup", t, func() {
+		ctx := createTestContext(t)
+
+		insertToken := func(token string, prev string, rotatedAt int64) {
+			ut := userAuthToken{AuthToken: token, PrevAuthToken: prev, RotatedAt: rotatedAt, UserAgent: "", ClientIp: ""}
+			_, err := ctx.sqlstore.NewSession().Insert(&ut)
+			So(err, ShouldBeNil)
+		}
+
+		// insert three old tokens that should be deleted
+		for i := 0; i < 3; i++ {
+			insertToken(fmt.Sprintf("oldA%d", i), fmt.Sprintf("oldB%d", i), int64(i))
+		}
+
+		// insert three active tokens that should not be deleted
+		for i := 0; i < 3; i++ {
+			insertToken(fmt.Sprintf("newA%d", i), fmt.Sprintf("newB%d", i), getTime().Unix())
+		}
+
+		affected, err := ctx.tokenService.deleteOldSession(time.Hour)
+		So(err, ShouldBeNil)
+		So(affected, ShouldEqual, 3)
+	})
+}

+ 0 - 2
pkg/services/session/session.go

@@ -14,8 +14,6 @@ import (
 
 
 const (
 const (
 	SESS_KEY_USERID       = "uid"
 	SESS_KEY_USERID       = "uid"
-	SESS_KEY_OAUTH_STATE  = "state"
-	SESS_KEY_APIKEY       = "apikey_id" // used for render requests with api keys
 	SESS_KEY_LASTLDAPSYNC = "last_ldap_sync"
 	SESS_KEY_LASTLDAPSYNC = "last_ldap_sync"
 )
 )
 
 

+ 1 - 0
pkg/services/sqlstore/migrations/migrations.go

@@ -32,6 +32,7 @@ func AddMigrations(mg *Migrator) {
 	addLoginAttemptMigrations(mg)
 	addLoginAttemptMigrations(mg)
 	addUserAuthMigrations(mg)
 	addUserAuthMigrations(mg)
 	addServerlockMigrations(mg)
 	addServerlockMigrations(mg)
+	addUserAuthTokenMigrations(mg)
 }
 }
 
 
 func addMigrationLogMigrations(mg *Migrator) {
 func addMigrationLogMigrations(mg *Migrator) {

+ 32 - 0
pkg/services/sqlstore/migrations/user_auth_token_mig.go

@@ -0,0 +1,32 @@
+package migrations
+
+import (
+	. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
+)
+
+func addUserAuthTokenMigrations(mg *Migrator) {
+	userAuthTokenV1 := Table{
+		Name: "user_auth_token",
+		Columns: []*Column{
+			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
+			{Name: "user_id", Type: DB_BigInt, Nullable: false},
+			{Name: "auth_token", Type: DB_NVarchar, Length: 100, Nullable: false},
+			{Name: "prev_auth_token", Type: DB_NVarchar, Length: 100, Nullable: false},
+			{Name: "user_agent", Type: DB_NVarchar, Length: 255, Nullable: false},
+			{Name: "client_ip", Type: DB_NVarchar, Length: 255, Nullable: false},
+			{Name: "auth_token_seen", Type: DB_Bool, Nullable: false},
+			{Name: "seen_at", Type: DB_Int, Nullable: true},
+			{Name: "rotated_at", Type: DB_Int, Nullable: false},
+			{Name: "created_at", Type: DB_Int, Nullable: false},
+			{Name: "updated_at", Type: DB_Int, Nullable: false},
+		},
+		Indices: []*Index{
+			{Cols: []string{"auth_token"}, Type: UniqueIndex},
+			{Cols: []string{"prev_auth_token"}, Type: UniqueIndex},
+		},
+	}
+
+	mg.AddMigration("create user auth token table", NewAddTableMigration(userAuthTokenV1))
+	mg.AddMigration("add unique index user_auth_token.auth_token", NewAddIndexMigration(userAuthTokenV1, userAuthTokenV1.Indices[0]))
+	mg.AddMigration("add unique index user_auth_token.prev_auth_token", NewAddIndexMigration(userAuthTokenV1, userAuthTokenV1.Indices[1]))
+}

+ 18 - 6
pkg/setting/setting.go

@@ -83,9 +83,6 @@ var (
 
 
 	// Security settings.
 	// Security settings.
 	SecretKey                        string
 	SecretKey                        string
-	LogInRememberDays                int
-	CookieUserName                   string
-	CookieRememberName               string
 	DisableGravatar                  bool
 	DisableGravatar                  bool
 	EmailCodeValidMinutes            int
 	EmailCodeValidMinutes            int
 	DataProxyWhiteList               map[string]bool
 	DataProxyWhiteList               map[string]bool
@@ -224,6 +221,13 @@ type Cfg struct {
 	EnableAlphaPanels                bool
 	EnableAlphaPanels                bool
 	DisableSanitizeHtml              bool
 	DisableSanitizeHtml              bool
 	EnterpriseLicensePath            string
 	EnterpriseLicensePath            string
+
+	LoginCookieName                   string
+	LoginCookieMaxDays                int
+	LoginCookieRotation               int
+	LoginDeleteExpiredTokensAfterDays int
+
+	SecurityHTTPSCookies bool
 }
 }
 
 
 type CommandLineArgs struct {
 type CommandLineArgs struct {
@@ -547,6 +551,16 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
 		ApplicationName = APP_NAME_ENTERPRISE
 		ApplicationName = APP_NAME_ENTERPRISE
 	}
 	}
 
 
+	//login
+	login := iniFile.Section("login")
+	cfg.LoginCookieName = login.Key("cookie_name").MustString("grafana_session")
+	cfg.LoginCookieMaxDays = login.Key("login_remember_days").MustInt(7)
+	cfg.LoginDeleteExpiredTokensAfterDays = login.Key("delete_expired_token_after_days").MustInt(30)
+	cfg.LoginCookieRotation = login.Key("rotate_token_minutes").MustInt(10)
+	if cfg.LoginCookieRotation < 2 {
+		cfg.LoginCookieRotation = 2
+	}
+
 	Env = iniFile.Section("").Key("app_mode").MustString("development")
 	Env = iniFile.Section("").Key("app_mode").MustString("development")
 	InstanceName = iniFile.Section("").Key("instance_name").MustString("unknown_instance_name")
 	InstanceName = iniFile.Section("").Key("instance_name").MustString("unknown_instance_name")
 	PluginsPath = makeAbsolute(iniFile.Section("paths").Key("plugins").String(), HomePath)
 	PluginsPath = makeAbsolute(iniFile.Section("paths").Key("plugins").String(), HomePath)
@@ -587,11 +601,9 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
 	// read security settings
 	// read security settings
 	security := iniFile.Section("security")
 	security := iniFile.Section("security")
 	SecretKey = security.Key("secret_key").String()
 	SecretKey = security.Key("secret_key").String()
-	LogInRememberDays = security.Key("login_remember_days").MustInt()
-	CookieUserName = security.Key("cookie_username").String()
-	CookieRememberName = security.Key("cookie_remember_name").String()
 	DisableGravatar = security.Key("disable_gravatar").MustBool(true)
 	DisableGravatar = security.Key("disable_gravatar").MustBool(true)
 	cfg.DisableBruteForceLoginProtection = security.Key("disable_brute_force_login_protection").MustBool(false)
 	cfg.DisableBruteForceLoginProtection = security.Key("disable_brute_force_login_protection").MustBool(false)
+	cfg.SecurityHTTPSCookies = security.Key("https_flag_cookies").MustBool(false)
 	DisableBruteForceLoginProtection = cfg.DisableBruteForceLoginProtection
 	DisableBruteForceLoginProtection = cfg.DisableBruteForceLoginProtection
 
 
 	// read snapshots settings
 	// read snapshots settings

+ 8 - 0
pkg/util/encoding.go

@@ -101,3 +101,11 @@ func DecodeBasicAuthHeader(header string) (string, string, error) {
 
 
 	return userAndPass[0], userAndPass[1], nil
 	return userAndPass[0], userAndPass[1], nil
 }
 }
+
+func RandomHex(n int) (string, error) {
+	bytes := make([]byte, n)
+	if _, err := rand.Read(bytes); err != nil {
+		return "", err
+	}
+	return hex.EncodeToString(bytes), nil
+}

+ 29 - 0
pkg/util/ip_address.go

@@ -0,0 +1,29 @@
+package util
+
+import (
+	"net"
+	"strings"
+)
+
+// ParseIPAddress parses an IP address and removes port and/or IPV6 format
+func ParseIPAddress(input string) string {
+	s := input
+	lastIndex := strings.LastIndex(input, ":")
+
+	if lastIndex != -1 {
+		if lastIndex > 0 && input[lastIndex-1:lastIndex] != ":" {
+			s = input[:lastIndex]
+		}
+	}
+
+	s = strings.Replace(s, "[", "", -1)
+	s = strings.Replace(s, "]", "", -1)
+
+	ip := net.ParseIP(s)
+
+	if ip.IsLoopback() {
+		return "127.0.0.1"
+	}
+
+	return ip.String()
+}

+ 16 - 0
pkg/util/ip_address_test.go

@@ -0,0 +1,16 @@
+package util
+
+import (
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestParseIPAddress(t *testing.T) {
+	Convey("Test parse ip address", t, func() {
+		So(ParseIPAddress("192.168.0.140:456"), ShouldEqual, "192.168.0.140")
+		So(ParseIPAddress("[::1:456]"), ShouldEqual, "127.0.0.1")
+		So(ParseIPAddress("[::1]"), ShouldEqual, "127.0.0.1")
+		So(ParseIPAddress("192.168.0.140"), ShouldEqual, "192.168.0.140")
+	})
+}