mirror of
https://github.com/grafana/grafana.git
synced 2025-02-13 00:55:47 -06:00
Oauth2 Updates (#6226)
* break out go and js build commands * support oauth providers that return errors via redirect * remove extra call to get grafana.net org membership * removed GitHub specifics from generic OAuth * readded ability to name generic source * revert to a backward-compatible state, refactor and clean up * streamline oauth user creation, make generic oauth support more generic
This commit is contained in:
parent
d4fd1c82e3
commit
6b16fcea52
18
Makefile
18
Makefile
@ -1,16 +1,28 @@
|
||||
all: deps build
|
||||
|
||||
deps:
|
||||
deps-go:
|
||||
go run build.go setup
|
||||
|
||||
deps-js:
|
||||
npm install
|
||||
|
||||
build:
|
||||
deps: deps-go deps-js
|
||||
|
||||
build-go:
|
||||
go run build.go build
|
||||
|
||||
build-js:
|
||||
npm run build
|
||||
|
||||
test:
|
||||
build: build-go build-js
|
||||
|
||||
test-go:
|
||||
go test -v ./pkg/...
|
||||
|
||||
test-js:
|
||||
npm test
|
||||
|
||||
test: test-go test-js
|
||||
|
||||
run:
|
||||
./bin/grafana-server
|
||||
|
@ -1,11 +1,17 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
@ -17,9 +23,9 @@ import (
|
||||
)
|
||||
|
||||
func GenStateString() string {
|
||||
rnd := make([]byte, 32)
|
||||
rand.Read(rnd)
|
||||
return base64.StdEncoding.EncodeToString(rnd)
|
||||
rnd := make([]byte, 32)
|
||||
rand.Read(rnd)
|
||||
return base64.StdEncoding.EncodeToString(rnd)
|
||||
}
|
||||
|
||||
func OAuthLogin(ctx *middleware.Context) {
|
||||
@ -35,6 +41,14 @@ func OAuthLogin(ctx *middleware.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
error := ctx.Query("error")
|
||||
if error != "" {
|
||||
errorDesc := ctx.Query("error_description")
|
||||
ctx.Logger.Info("OAuthLogin Failed", "error", error, "errorDesc", errorDesc)
|
||||
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1003")
|
||||
return
|
||||
}
|
||||
|
||||
code := ctx.Query("code")
|
||||
if code == "" {
|
||||
state := GenStateString()
|
||||
@ -52,7 +66,38 @@ func OAuthLogin(ctx *middleware.Context) {
|
||||
}
|
||||
|
||||
// handle call back
|
||||
token, err := connect.Exchange(oauth2.NoContext, code)
|
||||
|
||||
// initialize oauth2 context
|
||||
oauthCtx := oauth2.NoContext
|
||||
if setting.OAuthService.OAuthInfos[name].TlsClientCert != "" {
|
||||
cert, err := tls.LoadX509KeyPair(setting.OAuthService.OAuthInfos[name].TlsClientCert, setting.OAuthService.OAuthInfos[name].TlsClientKey)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Load CA cert
|
||||
caCert, err := ioutil.ReadFile(setting.OAuthService.OAuthInfos[name].TlsClientCa)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
RootCAs: caCertPool,
|
||||
},
|
||||
}
|
||||
sslcli := &http.Client{Transport: tr}
|
||||
|
||||
oauthCtx = context.TODO()
|
||||
oauthCtx = context.WithValue(oauthCtx, oauth2.HTTPClient, sslcli)
|
||||
}
|
||||
|
||||
// get token from provider
|
||||
token, err := connect.Exchange(oauthCtx, code)
|
||||
if err != nil {
|
||||
ctx.Handle(500, "login.OAuthLogin(NewTransportWithCode)", err)
|
||||
return
|
||||
@ -60,7 +105,11 @@ func OAuthLogin(ctx *middleware.Context) {
|
||||
|
||||
ctx.Logger.Debug("OAuthLogin Got token")
|
||||
|
||||
userInfo, err := connect.UserInfo(token)
|
||||
// set up oauth2 client
|
||||
client := connect.Client(oauthCtx, token)
|
||||
|
||||
// get user info
|
||||
userInfo, err := connect.UserInfo(client)
|
||||
if err != nil {
|
||||
if err == social.ErrMissingTeamMembership {
|
||||
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1000")
|
||||
@ -100,7 +149,7 @@ func OAuthLogin(ctx *middleware.Context) {
|
||||
return
|
||||
}
|
||||
cmd := m.CreateUserCommand{
|
||||
Login: userInfo.Email,
|
||||
Login: userInfo.Login,
|
||||
Email: userInfo.Email,
|
||||
Name: userInfo.Name,
|
||||
Company: userInfo.Company,
|
||||
|
@ -9,6 +9,9 @@ type OAuthInfo struct {
|
||||
ApiUrl string
|
||||
AllowSignup bool
|
||||
Name string
|
||||
TlsClientCert string
|
||||
TlsClientKey string
|
||||
TlsClientCa string
|
||||
}
|
||||
|
||||
type OAuther struct {
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
@ -160,15 +159,16 @@ func (s *GenericOAuth) FetchOrganizations(client *http.Client) ([]string, error)
|
||||
return logins, nil
|
||||
}
|
||||
|
||||
func (s *GenericOAuth) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
|
||||
func (s *GenericOAuth) UserInfo(client *http.Client) (*BasicUserInfo, error) {
|
||||
var data struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Login string `json:"login"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Attributes map[string][]string `json:"attributes"`
|
||||
}
|
||||
|
||||
var err error
|
||||
client := s.Client(oauth2.NoContext, token)
|
||||
r, err := client.Get(s.apiUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -181,11 +181,30 @@ func (s *GenericOAuth) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
|
||||
}
|
||||
|
||||
userInfo := &BasicUserInfo{
|
||||
Identity: strconv.Itoa(data.Id),
|
||||
Name: data.Name,
|
||||
Login: data.Login,
|
||||
Email: data.Email,
|
||||
}
|
||||
|
||||
if (userInfo.Email == "" && data.Attributes["email:primary"] != nil) {
|
||||
userInfo.Email = data.Attributes["email:primary"][0]
|
||||
}
|
||||
|
||||
if userInfo.Email == "" {
|
||||
userInfo.Email, err = s.FetchPrivateEmail(client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if (userInfo.Login == "" && data.Username != "") {
|
||||
userInfo.Login = data.Username
|
||||
}
|
||||
|
||||
if (userInfo.Login == "") {
|
||||
userInfo.Login = data.Email
|
||||
}
|
||||
|
||||
if !s.IsTeamMember(client) {
|
||||
return nil, errors.New("User not a member of one of the required teams")
|
||||
}
|
||||
@ -194,12 +213,5 @@ func (s *GenericOAuth) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
|
||||
return nil, errors.New("User not a member of one of the required organizations")
|
||||
}
|
||||
|
||||
if userInfo.Email == "" {
|
||||
userInfo.Email, err = s.FetchPrivateEmail(client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
@ -168,15 +167,14 @@ func (s *SocialGithub) FetchOrganizations(client *http.Client) ([]string, error)
|
||||
return logins, nil
|
||||
}
|
||||
|
||||
func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
|
||||
func (s *SocialGithub) UserInfo(client *http.Client) (*BasicUserInfo, error) {
|
||||
var data struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"login"`
|
||||
Login string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
var err error
|
||||
client := s.Client(oauth2.NoContext, token)
|
||||
r, err := client.Get(s.apiUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -189,8 +187,8 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
|
||||
}
|
||||
|
||||
userInfo := &BasicUserInfo{
|
||||
Identity: strconv.Itoa(data.Id),
|
||||
Name: data.Name,
|
||||
Name: data.Login,
|
||||
Login: data.Login,
|
||||
Email: data.Email,
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ package social
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
@ -27,15 +28,13 @@ func (s *SocialGoogle) IsSignupAllowed() bool {
|
||||
return s.allowSignup
|
||||
}
|
||||
|
||||
func (s *SocialGoogle) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
|
||||
func (s *SocialGoogle) UserInfo(client *http.Client) (*BasicUserInfo, error) {
|
||||
var data struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
var err error
|
||||
|
||||
client := s.Client(oauth2.NoContext, token)
|
||||
r, err := client.Get(s.apiUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -45,7 +44,6 @@ func (s *SocialGoogle) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
|
||||
return nil, err
|
||||
}
|
||||
return &BasicUserInfo{
|
||||
Identity: data.Id,
|
||||
Name: data.Name,
|
||||
Email: data.Email,
|
||||
}, nil
|
||||
|
@ -2,9 +2,7 @@ package social
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
@ -18,6 +16,10 @@ type SocialGrafanaNet struct {
|
||||
allowSignup bool
|
||||
}
|
||||
|
||||
type OrgRecord struct {
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
||||
func (s *SocialGrafanaNet) Type() int {
|
||||
return int(models.GRAFANANET)
|
||||
}
|
||||
@ -30,19 +32,14 @@ func (s *SocialGrafanaNet) IsSignupAllowed() bool {
|
||||
return s.allowSignup
|
||||
}
|
||||
|
||||
func (s *SocialGrafanaNet) IsOrganizationMember(client *http.Client) bool {
|
||||
func (s *SocialGrafanaNet) IsOrganizationMember(organizations []OrgRecord) bool {
|
||||
if len(s.allowedOrganizations) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
organizations, err := s.FetchOrganizations(client)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, allowedOrganization := range s.allowedOrganizations {
|
||||
for _, organization := range organizations {
|
||||
if organization == allowedOrganization {
|
||||
if organization.Login == allowedOrganization {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -51,43 +48,16 @@ func (s *SocialGrafanaNet) IsOrganizationMember(client *http.Client) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *SocialGrafanaNet) FetchOrganizations(client *http.Client) ([]string, error) {
|
||||
type Record struct {
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
||||
url := fmt.Sprintf(s.url + "/api/oauth2/user/orgs")
|
||||
r, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
var records []Record
|
||||
|
||||
if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var logins = make([]string, len(records))
|
||||
for i, record := range records {
|
||||
logins[i] = record.Login
|
||||
}
|
||||
|
||||
return logins, nil
|
||||
}
|
||||
|
||||
func (s *SocialGrafanaNet) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
|
||||
func (s *SocialGrafanaNet) UserInfo(client *http.Client) (*BasicUserInfo, error) {
|
||||
var data struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"login"`
|
||||
Name string `json:"name"`
|
||||
Login string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
Orgs []OrgRecord `json:"orgs"`
|
||||
}
|
||||
|
||||
var err error
|
||||
client := s.Client(oauth2.NoContext, token)
|
||||
r, err := client.Get(s.url + "/api/oauth2/user")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -100,13 +70,13 @@ func (s *SocialGrafanaNet) UserInfo(token *oauth2.Token) (*BasicUserInfo, error)
|
||||
}
|
||||
|
||||
userInfo := &BasicUserInfo{
|
||||
Identity: strconv.Itoa(data.Id),
|
||||
Name: data.Name,
|
||||
Login: data.Login,
|
||||
Email: data.Email,
|
||||
Role: data.Role,
|
||||
}
|
||||
|
||||
if !s.IsOrganizationMember(client) {
|
||||
if !s.IsOrganizationMember(data.Orgs) {
|
||||
return nil, ErrMissingOrganizationMembership
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,16 @@
|
||||
package social
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type BasicUserInfo struct {
|
||||
Identity string
|
||||
Name string
|
||||
Email string
|
||||
Login string
|
||||
@ -20,12 +20,13 @@ type BasicUserInfo struct {
|
||||
|
||||
type SocialConnector interface {
|
||||
Type() int
|
||||
UserInfo(token *oauth2.Token) (*BasicUserInfo, error)
|
||||
UserInfo(client *http.Client) (*BasicUserInfo, error)
|
||||
IsEmailAllowed(email string) bool
|
||||
IsSignupAllowed() bool
|
||||
|
||||
AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string
|
||||
Exchange(ctx context.Context, code string) (*oauth2.Token, error)
|
||||
Client(ctx context.Context, t *oauth2.Token) *http.Client
|
||||
}
|
||||
|
||||
var (
|
||||
@ -52,6 +53,9 @@ func NewOAuthService() {
|
||||
AllowedDomains: sec.Key("allowed_domains").Strings(" "),
|
||||
AllowSignup: sec.Key("allow_sign_up").MustBool(),
|
||||
Name: sec.Key("name").MustString(name),
|
||||
TlsClientCert: sec.Key("tls_client_cert").String(),
|
||||
TlsClientKey: sec.Key("tls_client_key").String(),
|
||||
TlsClientCa: sec.Key("tls_client_ca").String(),
|
||||
}
|
||||
|
||||
if !info.Enabled {
|
||||
@ -59,6 +63,7 @@ func NewOAuthService() {
|
||||
}
|
||||
|
||||
setting.OAuthService.OAuthInfos[name] = info
|
||||
|
||||
config := oauth2.Config{
|
||||
ClientID: info.ClientId,
|
||||
ClientSecret: info.ClientSecret,
|
||||
@ -85,9 +90,10 @@ func NewOAuthService() {
|
||||
// Google.
|
||||
if name == "google" {
|
||||
SocialMap["google"] = &SocialGoogle{
|
||||
Config: &config, allowedDomains: info.AllowedDomains,
|
||||
apiUrl: info.ApiUrl,
|
||||
allowSignup: info.AllowSignup,
|
||||
Config: &config,
|
||||
allowedDomains: info.AllowedDomains,
|
||||
apiUrl: info.ApiUrl,
|
||||
allowSignup: info.AllowSignup,
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,15 +110,15 @@ func NewOAuthService() {
|
||||
}
|
||||
|
||||
if name == "grafananet" {
|
||||
config := oauth2.Config{
|
||||
config = oauth2.Config{
|
||||
ClientID: info.ClientId,
|
||||
ClientSecret: info.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: setting.GrafanaNetUrl + "/oauth2/authorize",
|
||||
TokenURL: setting.GrafanaNetUrl + "/api/oauth2/token",
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: setting.GrafanaNetUrl + "/oauth2/authorize",
|
||||
TokenURL: setting.GrafanaNetUrl + "/api/oauth2/token",
|
||||
},
|
||||
RedirectURL: strings.TrimSuffix(setting.AppUrl, "/") + SocialBaseUrl + name,
|
||||
Scopes: info.Scopes,
|
||||
RedirectURL: strings.TrimSuffix(setting.AppUrl, "/") + SocialBaseUrl + name,
|
||||
Scopes: info.Scopes,
|
||||
}
|
||||
|
||||
SocialMap["grafananet"] = &SocialGrafanaNet{
|
||||
|
@ -11,6 +11,7 @@ function (angular, _, coreModule, config) {
|
||||
"1000": "Required team membership not fulfilled",
|
||||
"1001": "Required organization membership not fulfilled",
|
||||
"1002": "Required email domain not fulfilled",
|
||||
"1003": "Login provider denied login request",
|
||||
};
|
||||
|
||||
coreModule.default.controller('LoginCtrl', function($scope, backendSrv, contextSrv, $location) {
|
||||
|
Loading…
Reference in New Issue
Block a user