From 04e17c145fb61312758eaa5af40df71745a095f8 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Thu, 18 Jan 2018 17:17:51 -0500 Subject: [PATCH] support for decoding JWT id tokens --- pkg/api/login_oauth.go | 16 ++++--- pkg/social/generic_oauth.go | 76 ++++++++++++++++++++++++--------- pkg/social/github_oauth.go | 4 +- pkg/social/google_oauth.go | 4 +- pkg/social/grafana_com_oauth.go | 4 +- pkg/social/social.go | 32 +++++++++++--- 6 files changed, 97 insertions(+), 39 deletions(-) diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 38aff2ac2b4..016207f7ce7 100644 --- a/pkg/api/login_oauth.go +++ b/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") diff --git a/pkg/social/generic_oauth.go b/pkg/social/generic_oauth.go index 34830c2a348..36b65ce2f4d 100644 --- a/pkg/social/generic_oauth.go +++ b/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 } diff --git a/pkg/social/github_oauth.go b/pkg/social/github_oauth.go index 7e348e2363a..6f00cd2cd9e 100644 --- a/pkg/social/github_oauth.go +++ b/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"` diff --git a/pkg/social/google_oauth.go b/pkg/social/google_oauth.go index c44720fbe21..e9ab08305f6 100644 --- a/pkg/social/google_oauth.go +++ b/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"` diff --git a/pkg/social/grafana_com_oauth.go b/pkg/social/grafana_com_oauth.go index 9cc87d4b41c..d3614520d61 100644 --- a/pkg/social/grafana_com_oauth.go +++ b/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"` diff --git a/pkg/social/social.go b/pkg/social/social.go index 8e877ece4f1..9f2fac4b09a 100644 --- a/pkg/social/social.go +++ b/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()),