support for decoding JWT id tokens

This commit is contained in:
Dan Cech 2018-01-18 17:17:51 -05:00
parent 4720b86f5c
commit 04e17c145f
No known key found for this signature in database
GPG Key ID: 6F1146C5B66FBD41
6 changed files with 97 additions and 39 deletions

View File

@ -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")

View File

@ -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
}

View File

@ -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"`

View File

@ -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"`

View File

@ -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"`

View File

@ -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()),