Forráskód Böngészése

support for decoding JWT id tokens

Dan Cech 8 éve
szülő
commit
04e17c145f

+ 10 - 6
pkg/api/login_oauth.go

@@ -96,7 +96,9 @@ func OAuthLogin(ctx *middleware.Context) {
 	if setting.OAuthService.OAuthInfos[name].TlsClientCert != "" || setting.OAuthService.OAuthInfos[name].TlsClientKey != "" {
 		cert, err := tls.LoadX509KeyPair(setting.OAuthService.OAuthInfos[name].TlsClientCert, setting.OAuthService.OAuthInfos[name].TlsClientKey)
 		if err != nil {
-			log.Fatal(1, "Failed to setup TlsClientCert", "oauth provider", name, "error", err)
+			oauthLogger.Error("Failed to setup TlsClientCert", "oauth provider", name, "error", err)
+			ctx.Handle(500, "login.OAuthLogin(Failed to setup TlsClientCert)", nil)
+			return
 		}
 
 		tr.TLSClientConfig.Certificates = append(tr.TLSClientConfig.Certificates, cert)
@@ -105,7 +107,9 @@ func OAuthLogin(ctx *middleware.Context) {
 	if setting.OAuthService.OAuthInfos[name].TlsClientCa != "" {
 		caCert, err := ioutil.ReadFile(setting.OAuthService.OAuthInfos[name].TlsClientCa)
 		if err != nil {
-			log.Fatal(1, "Failed to setup TlsClientCa", "oauth provider", name, "error", err)
+			oauthLogger.Error("Failed to setup TlsClientCa", "oauth provider", name, "error", err)
+			ctx.Handle(500, "login.OAuthLogin(Failed to setup TlsClientCa)", nil)
+			return
 		}
 		caCertPool := x509.NewCertPool()
 		caCertPool.AppendCertsFromPEM(caCert)
@@ -124,13 +128,13 @@ func OAuthLogin(ctx *middleware.Context) {
 	// token.TokenType was defaulting to "bearer", which is out of spec, so we explicitly set to "Bearer"
 	token.TokenType = "Bearer"
 
-	ctx.Logger.Debug("OAuthLogin Got token")
+	oauthLogger.Debug("OAuthLogin Got token", "token", token)
 
 	// set up oauth2 client
 	client := connect.Client(oauthCtx, token)
 
 	// get user info
-	userInfo, err := connect.UserInfo(client)
+	userInfo, err := connect.UserInfo(client, token)
 	if err != nil {
 		if sErr, ok := err.(*social.Error); ok {
 			redirectWithError(ctx, sErr)
@@ -140,7 +144,7 @@ func OAuthLogin(ctx *middleware.Context) {
 		return
 	}
 
-	ctx.Logger.Debug("OAuthLogin got user info", "userInfo", userInfo)
+	oauthLogger.Debug("OAuthLogin got user info", "userInfo", userInfo)
 
 	// validate that we got at least an email address
 	if userInfo.Email == "" {
@@ -205,7 +209,7 @@ func OAuthLogin(ctx *middleware.Context) {
 }
 
 func redirectWithError(ctx *middleware.Context, err error, v ...interface{}) {
-	ctx.Logger.Info(err.Error(), v...)
+	oauthLogger.Info(err.Error(), v...)
 	// TODO: we can use the flash storage here once it's implemented
 	ctx.Session.Set("loginError", err.Error())
 	ctx.Redirect(setting.AppSubUrl + "/login")

+ 55 - 21
pkg/social/generic_oauth.go

@@ -1,19 +1,21 @@
 package social
 
 import (
+	"encoding/base64"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"net/http"
 	"net/mail"
+	"regexp"
 
 	"github.com/grafana/grafana/pkg/models"
 
 	"golang.org/x/oauth2"
 )
 
-type GenericOAuth struct {
-	*oauth2.Config
+type SocialGenericOAuth struct {
+	*SocialBase
 	allowedDomains       []string
 	allowedOrganizations []string
 	apiUrl               string
@@ -21,19 +23,19 @@ type GenericOAuth struct {
 	teamIds              []int
 }
 
-func (s *GenericOAuth) Type() int {
+func (s *SocialGenericOAuth) Type() int {
 	return int(models.GENERIC)
 }
 
-func (s *GenericOAuth) IsEmailAllowed(email string) bool {
+func (s *SocialGenericOAuth) IsEmailAllowed(email string) bool {
 	return isEmailAllowed(email, s.allowedDomains)
 }
 
-func (s *GenericOAuth) IsSignupAllowed() bool {
+func (s *SocialGenericOAuth) IsSignupAllowed() bool {
 	return s.allowSignup
 }
 
-func (s *GenericOAuth) IsTeamMember(client *http.Client) bool {
+func (s *SocialGenericOAuth) IsTeamMember(client *http.Client) bool {
 	if len(s.teamIds) == 0 {
 		return true
 	}
@@ -54,7 +56,7 @@ func (s *GenericOAuth) IsTeamMember(client *http.Client) bool {
 	return false
 }
 
-func (s *GenericOAuth) IsOrganizationMember(client *http.Client) bool {
+func (s *SocialGenericOAuth) IsOrganizationMember(client *http.Client) bool {
 	if len(s.allowedOrganizations) == 0 {
 		return true
 	}
@@ -75,7 +77,7 @@ func (s *GenericOAuth) IsOrganizationMember(client *http.Client) bool {
 	return false
 }
 
-func (s *GenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) {
+func (s *SocialGenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) {
 	type Record struct {
 		Email       string `json:"email"`
 		Primary     bool   `json:"primary"`
@@ -116,7 +118,7 @@ func (s *GenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) {
 	return email, nil
 }
 
-func (s *GenericOAuth) FetchTeamMemberships(client *http.Client) ([]int, error) {
+func (s *SocialGenericOAuth) FetchTeamMemberships(client *http.Client) ([]int, error) {
 	type Record struct {
 		Id int `json:"id"`
 	}
@@ -141,7 +143,7 @@ func (s *GenericOAuth) FetchTeamMemberships(client *http.Client) ([]int, error)
 	return ids, nil
 }
 
-func (s *GenericOAuth) FetchOrganizations(client *http.Client) ([]string, error) {
+func (s *SocialGenericOAuth) FetchOrganizations(client *http.Client) ([]string, error) {
 	type Record struct {
 		Login string `json:"login"`
 	}
@@ -176,17 +178,19 @@ type UserInfoJson struct {
 	Attributes  map[string][]string `json:"attributes"`
 }
 
-func (s *GenericOAuth) UserInfo(client *http.Client) (*BasicUserInfo, error) {
+func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
 	var data UserInfoJson
 
-	response, err := HttpGet(client, s.apiUrl)
-	if err != nil {
-		return nil, fmt.Errorf("Error getting user info: %s", err)
-	}
+	if s.extractToken(&data, token) != true {
+		response, err := HttpGet(client, s.apiUrl)
+		if err != nil {
+			return nil, fmt.Errorf("Error getting user info: %s", err)
+		}
 
-	err = json.Unmarshal(response.Body, &data)
-	if err != nil {
-		return nil, fmt.Errorf("Error getting user info: %s", err)
+		err = json.Unmarshal(response.Body, &data)
+		if err != nil {
+			return nil, fmt.Errorf("Error decoding user info JSON: %s", err)
+		}
 	}
 
 	name, err := s.extractName(data)
@@ -221,7 +225,37 @@ func (s *GenericOAuth) UserInfo(client *http.Client) (*BasicUserInfo, error) {
 	return userInfo, nil
 }
 
-func (s *GenericOAuth) extractEmail(data UserInfoJson, client *http.Client) (string, error) {
+func (s *SocialGenericOAuth) extractToken(data *UserInfoJson, token *oauth2.Token) (bool) {
+	idToken := token.Extra("id_token")
+	if idToken == nil {
+		s.log.Debug("No id_token found", "token", token)
+		return false
+	}
+
+	jwtRegexp := regexp.MustCompile("^([-_a-zA-Z0-9]+)[.]([-_a-zA-Z0-9]+)[.]([-_a-zA-Z0-9]+)$")
+	matched := jwtRegexp.FindStringSubmatch(idToken.(string))
+	if matched == nil {
+		s.log.Debug("id_token is not in JWT format", "id_token", idToken.(string))
+		return false
+	}
+
+	payload, err := base64.RawURLEncoding.DecodeString(matched[2])
+	if err != nil {
+		s.log.Error("Error base64 decoding id_token", "raw_payload", matched[2], "err", err)
+		return false
+	}
+
+	err = json.Unmarshal(payload, data)
+	if err != nil {
+		s.log.Error("Error decoding id_token JSON", "payload", string(payload), "err", err)
+		return false
+	}
+
+	s.log.Debug("Received id_token", "json", string(payload), "data", data)
+	return true
+}
+
+func (s *SocialGenericOAuth) extractEmail(data UserInfoJson, client *http.Client) (string, error) {
 	if data.Email != "" {
 		return data.Email, nil
 	}
@@ -240,7 +274,7 @@ func (s *GenericOAuth) extractEmail(data UserInfoJson, client *http.Client) (str
 	return s.FetchPrivateEmail(client)
 }
 
-func (s *GenericOAuth) extractLogin(data UserInfoJson, email string) (string, error) {
+func (s *SocialGenericOAuth) extractLogin(data UserInfoJson, email string) (string, error) {
 	if data.Login != "" {
 		return data.Login, nil
 	}
@@ -252,7 +286,7 @@ func (s *GenericOAuth) extractLogin(data UserInfoJson, email string) (string, er
 	return email, nil
 }
 
-func (s *GenericOAuth) extractName(data UserInfoJson) (string, error) {
+func (s *SocialGenericOAuth) extractName(data UserInfoJson) (string, error) {
 	if data.Name != "" {
 		return data.Name, nil
 	}

+ 2 - 2
pkg/social/github_oauth.go

@@ -12,7 +12,7 @@ import (
 )
 
 type SocialGithub struct {
-	*oauth2.Config
+	*SocialBase
 	allowedDomains       []string
 	allowedOrganizations []string
 	apiUrl               string
@@ -192,7 +192,7 @@ func (s *SocialGithub) FetchOrganizations(client *http.Client, organizationsUrl
 	return logins, nil
 }
 
-func (s *SocialGithub) UserInfo(client *http.Client) (*BasicUserInfo, error) {
+func (s *SocialGithub) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
 
 	var data struct {
 		Id               int    `json:"id"`

+ 2 - 2
pkg/social/google_oauth.go

@@ -11,7 +11,7 @@ import (
 )
 
 type SocialGoogle struct {
-	*oauth2.Config
+	*SocialBase
 	allowedDomains []string
 	hostedDomain   string
 	apiUrl         string
@@ -30,7 +30,7 @@ func (s *SocialGoogle) IsSignupAllowed() bool {
 	return s.allowSignup
 }
 
-func (s *SocialGoogle) UserInfo(client *http.Client) (*BasicUserInfo, error) {
+func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
 	var data struct {
 		Name  string `json:"name"`
 		Email string `json:"email"`

+ 2 - 2
pkg/social/grafana_com_oauth.go

@@ -11,7 +11,7 @@ import (
 )
 
 type SocialGrafanaCom struct {
-	*oauth2.Config
+	*SocialBase
 	url                  string
 	allowedOrganizations []string
 	allowSignup          bool
@@ -49,7 +49,7 @@ func (s *SocialGrafanaCom) IsOrganizationMember(organizations []OrgRecord) bool
 	return false
 }
 
-func (s *SocialGrafanaCom) UserInfo(client *http.Client) (*BasicUserInfo, error) {
+func (s *SocialGrafanaCom) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
 	var data struct {
 		Name  string      `json:"name"`
 		Login string      `json:"username"`

+ 26 - 6
pkg/social/social.go

@@ -8,6 +8,7 @@ import (
 
 	"golang.org/x/oauth2"
 
+	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/util"
 )
@@ -22,7 +23,7 @@ type BasicUserInfo struct {
 
 type SocialConnector interface {
 	Type() int
-	UserInfo(client *http.Client) (*BasicUserInfo, error)
+	UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error)
 	IsEmailAllowed(email string) bool
 	IsSignupAllowed() bool
 
@@ -31,6 +32,11 @@ type SocialConnector interface {
 	Client(ctx context.Context, t *oauth2.Token) *http.Client
 }
 
+type SocialBase struct {
+	*oauth2.Config
+	log log.Logger
+}
+
 type Error struct {
 	s string
 }
@@ -91,10 +97,15 @@ func NewOAuthService() {
 			Scopes:      info.Scopes,
 		}
 
+		logger := log.New("oauth.login." + name)
+
 		// GitHub.
 		if name == "github" {
 			SocialMap["github"] = &SocialGithub{
-				Config:               &config,
+				SocialBase:           &SocialBase{
+					Config:               &config,
+					log:                  logger,
+				},
 				allowedDomains:       info.AllowedDomains,
 				apiUrl:               info.ApiUrl,
 				allowSignup:          info.AllowSignup,
@@ -106,7 +117,10 @@ func NewOAuthService() {
 		// Google.
 		if name == "google" {
 			SocialMap["google"] = &SocialGoogle{
-				Config:         &config,
+				SocialBase:           &SocialBase{
+					Config:               &config,
+					log:                  logger,
+				},
 				allowedDomains: info.AllowedDomains,
 				hostedDomain:   info.HostedDomain,
 				apiUrl:         info.ApiUrl,
@@ -116,8 +130,11 @@ func NewOAuthService() {
 
 		// Generic - Uses the same scheme as Github.
 		if name == "generic_oauth" {
-			SocialMap["generic_oauth"] = &GenericOAuth{
-				Config:               &config,
+			SocialMap["generic_oauth"] = &SocialGenericOAuth{
+				SocialBase:           &SocialBase{
+					Config:               &config,
+					log:                  logger,
+				},
 				allowedDomains:       info.AllowedDomains,
 				apiUrl:               info.ApiUrl,
 				allowSignup:          info.AllowSignup,
@@ -139,7 +156,10 @@ func NewOAuthService() {
 			}
 
 			SocialMap["grafana_com"] = &SocialGrafanaCom{
-				Config:               &config,
+				SocialBase:           &SocialBase{
+					Config:               &config,
+					log:                  logger,
+				},
 				url:                  setting.GrafanaComUrl,
 				allowSignup:          info.AllowSignup,
 				allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),