Commit a24aa3ae by Torkel Ödegaard

Merge branch 'generic-oauth-jwt' of https://github.com/DanCech/grafana

parents 1ce6a420 562aa580
...@@ -540,6 +540,70 @@ allowed_organizations = ...@@ -540,6 +540,70 @@ allowed_organizations =
allowed_organizations = allowed_organizations =
``` ```
### Set up oauth2 with Auth0
1. Create a new Client in Auth0
- Name: Grafana
- Type: Regular Web Application
2. Go to the Settings tab and set:
- Allowed Callback URLs: `https://<grafana domain>/login/generic_oauth`
3. Click Save Changes, then use the values at the top of the page to configure Grafana:
```bash
[auth.generic_oauth]
enabled = true
allow_sign_up = true
team_ids =
allowed_organizations =
name = Auth0
client_id = <client id>
client_secret = <client secret>
scopes = openid profile email
auth_url = https://<domain>/authorize
token_url = https://<domain>/oauth/token
api_url = https://<domain>/userinfo
```
### Set up oauth2 with Azure Active Directory
1. Log in to portal.azure.com and click "Azure Active Directory" in the side menu, then click the "Properties" sub-menu item.
2. Copy the "Directory ID", this is needed for setting URLs later
3. Click "App Registrations" and add a new application registration:
- Name: Grafana
- Application type: Web app / API
- Sign-on URL: `https://<grafana domain>/login/generic_oauth`
4. Click the name of the new application to open the application details page.
5. Note down the "Application ID", this will be the OAuth client id.
6. Click "Settings", then click "Keys" and add a new entry under Passwords
- Key Description: Grafana OAuth
- Duration: Never Expires
7. Click Save then copy the key value, this will be the OAuth client secret.
8. Configure Grafana as follows:
```bash
[auth.generic_oauth]
name = Azure AD
enabled = true
allow_sign_up = true
client_id = <application id>
client_secret = <key value>
scopes = openid email name
auth_url = https://login.microsoftonline.com/<directory id>/oauth2/authorize
token_url = https://login.microsoftonline.com/<directory id>/oauth2/token
api_url =
team_ids =
allowed_organizations =
```
<hr> <hr>
## [auth.basic] ## [auth.basic]
......
...@@ -96,7 +96,9 @@ func OAuthLogin(ctx *middleware.Context) { ...@@ -96,7 +96,9 @@ func OAuthLogin(ctx *middleware.Context) {
if setting.OAuthService.OAuthInfos[name].TlsClientCert != "" || setting.OAuthService.OAuthInfos[name].TlsClientKey != "" { 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) cert, err := tls.LoadX509KeyPair(setting.OAuthService.OAuthInfos[name].TlsClientCert, setting.OAuthService.OAuthInfos[name].TlsClientKey)
if err != nil { 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) tr.TLSClientConfig.Certificates = append(tr.TLSClientConfig.Certificates, cert)
...@@ -105,7 +107,9 @@ func OAuthLogin(ctx *middleware.Context) { ...@@ -105,7 +107,9 @@ func OAuthLogin(ctx *middleware.Context) {
if setting.OAuthService.OAuthInfos[name].TlsClientCa != "" { if setting.OAuthService.OAuthInfos[name].TlsClientCa != "" {
caCert, err := ioutil.ReadFile(setting.OAuthService.OAuthInfos[name].TlsClientCa) caCert, err := ioutil.ReadFile(setting.OAuthService.OAuthInfos[name].TlsClientCa)
if err != nil { 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 := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert) caCertPool.AppendCertsFromPEM(caCert)
...@@ -124,13 +128,13 @@ func OAuthLogin(ctx *middleware.Context) { ...@@ -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 was defaulting to "bearer", which is out of spec, so we explicitly set to "Bearer"
token.TokenType = "Bearer" token.TokenType = "Bearer"
ctx.Logger.Debug("OAuthLogin Got token") oauthLogger.Debug("OAuthLogin Got token", "token", token)
// set up oauth2 client // set up oauth2 client
client := connect.Client(oauthCtx, token) client := connect.Client(oauthCtx, token)
// get user info // get user info
userInfo, err := connect.UserInfo(client) 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) redirectWithError(ctx, sErr)
...@@ -140,7 +144,7 @@ func OAuthLogin(ctx *middleware.Context) { ...@@ -140,7 +144,7 @@ func OAuthLogin(ctx *middleware.Context) {
return 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 // validate that we got at least an email address
if userInfo.Email == "" { if userInfo.Email == "" {
...@@ -205,7 +209,7 @@ func OAuthLogin(ctx *middleware.Context) { ...@@ -205,7 +209,7 @@ func OAuthLogin(ctx *middleware.Context) {
} }
func redirectWithError(ctx *middleware.Context, err error, v ...interface{}) { 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 // TODO: we can use the flash storage here once it's implemented
ctx.Session.Set("loginError", err.Error()) ctx.Session.Set("loginError", err.Error())
ctx.Redirect(setting.AppSubUrl + "/login") ctx.Redirect(setting.AppSubUrl + "/login")
......
package social package social
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"net/mail" "net/mail"
"regexp"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
type GenericOAuth struct { type SocialGenericOAuth struct {
*oauth2.Config *SocialBase
allowedDomains []string allowedDomains []string
allowedOrganizations []string allowedOrganizations []string
apiUrl string apiUrl string
...@@ -21,19 +23,19 @@ type GenericOAuth struct { ...@@ -21,19 +23,19 @@ type GenericOAuth struct {
teamIds []int teamIds []int
} }
func (s *GenericOAuth) Type() int { func (s *SocialGenericOAuth) Type() int {
return int(models.GENERIC) return int(models.GENERIC)
} }
func (s *GenericOAuth) IsEmailAllowed(email string) bool { func (s *SocialGenericOAuth) IsEmailAllowed(email string) bool {
return isEmailAllowed(email, s.allowedDomains) return isEmailAllowed(email, s.allowedDomains)
} }
func (s *GenericOAuth) IsSignupAllowed() bool { func (s *SocialGenericOAuth) IsSignupAllowed() bool {
return s.allowSignup return s.allowSignup
} }
func (s *GenericOAuth) IsTeamMember(client *http.Client) bool { func (s *SocialGenericOAuth) IsTeamMember(client *http.Client) bool {
if len(s.teamIds) == 0 { if len(s.teamIds) == 0 {
return true return true
} }
...@@ -54,7 +56,7 @@ func (s *GenericOAuth) IsTeamMember(client *http.Client) bool { ...@@ -54,7 +56,7 @@ func (s *GenericOAuth) IsTeamMember(client *http.Client) bool {
return false return false
} }
func (s *GenericOAuth) IsOrganizationMember(client *http.Client) bool { func (s *SocialGenericOAuth) IsOrganizationMember(client *http.Client) bool {
if len(s.allowedOrganizations) == 0 { if len(s.allowedOrganizations) == 0 {
return true return true
} }
...@@ -75,7 +77,7 @@ func (s *GenericOAuth) IsOrganizationMember(client *http.Client) bool { ...@@ -75,7 +77,7 @@ func (s *GenericOAuth) IsOrganizationMember(client *http.Client) bool {
return false return false
} }
func (s *GenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) { func (s *SocialGenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) {
type Record struct { type Record struct {
Email string `json:"email"` Email string `json:"email"`
Primary bool `json:"primary"` Primary bool `json:"primary"`
...@@ -116,7 +118,7 @@ func (s *GenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) { ...@@ -116,7 +118,7 @@ func (s *GenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) {
return email, nil return email, nil
} }
func (s *GenericOAuth) FetchTeamMemberships(client *http.Client) ([]int, error) { func (s *SocialGenericOAuth) FetchTeamMemberships(client *http.Client) ([]int, error) {
type Record struct { type Record struct {
Id int `json:"id"` Id int `json:"id"`
} }
...@@ -141,7 +143,7 @@ func (s *GenericOAuth) FetchTeamMemberships(client *http.Client) ([]int, error) ...@@ -141,7 +143,7 @@ func (s *GenericOAuth) FetchTeamMemberships(client *http.Client) ([]int, error)
return ids, nil return ids, nil
} }
func (s *GenericOAuth) FetchOrganizations(client *http.Client) ([]string, error) { func (s *SocialGenericOAuth) FetchOrganizations(client *http.Client) ([]string, error) {
type Record struct { type Record struct {
Login string `json:"login"` Login string `json:"login"`
} }
...@@ -176,17 +178,19 @@ type UserInfoJson struct { ...@@ -176,17 +178,19 @@ type UserInfoJson struct {
Attributes map[string][]string `json:"attributes"` 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 var data UserInfoJson
response, err := HttpGet(client, s.apiUrl) if s.extractToken(&data, token) != true {
if err != nil { response, err := HttpGet(client, s.apiUrl)
return nil, fmt.Errorf("Error getting user info: %s", err) if err != nil {
} return nil, fmt.Errorf("Error getting user info: %s", err)
}
err = json.Unmarshal(response.Body, &data) err = json.Unmarshal(response.Body, &data)
if err != nil { if err != nil {
return nil, fmt.Errorf("Error getting user info: %s", err) return nil, fmt.Errorf("Error decoding user info JSON: %s", err)
}
} }
name, err := s.extractName(data) name, err := s.extractName(data)
...@@ -221,7 +225,37 @@ func (s *GenericOAuth) UserInfo(client *http.Client) (*BasicUserInfo, error) { ...@@ -221,7 +225,37 @@ func (s *GenericOAuth) UserInfo(client *http.Client) (*BasicUserInfo, error) {
return userInfo, nil 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 != "" { if data.Email != "" {
return data.Email, nil return data.Email, nil
} }
...@@ -240,7 +274,7 @@ func (s *GenericOAuth) extractEmail(data UserInfoJson, client *http.Client) (str ...@@ -240,7 +274,7 @@ func (s *GenericOAuth) extractEmail(data UserInfoJson, client *http.Client) (str
return s.FetchPrivateEmail(client) 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 != "" { if data.Login != "" {
return data.Login, nil return data.Login, nil
} }
...@@ -252,7 +286,7 @@ func (s *GenericOAuth) extractLogin(data UserInfoJson, email string) (string, er ...@@ -252,7 +286,7 @@ func (s *GenericOAuth) extractLogin(data UserInfoJson, email string) (string, er
return email, nil return email, nil
} }
func (s *GenericOAuth) extractName(data UserInfoJson) (string, error) { func (s *SocialGenericOAuth) extractName(data UserInfoJson) (string, error) {
if data.Name != "" { if data.Name != "" {
return data.Name, nil return data.Name, nil
} }
......
...@@ -12,7 +12,7 @@ import ( ...@@ -12,7 +12,7 @@ import (
) )
type SocialGithub struct { type SocialGithub struct {
*oauth2.Config *SocialBase
allowedDomains []string allowedDomains []string
allowedOrganizations []string allowedOrganizations []string
apiUrl string apiUrl string
...@@ -192,7 +192,7 @@ func (s *SocialGithub) FetchOrganizations(client *http.Client, organizationsUrl ...@@ -192,7 +192,7 @@ func (s *SocialGithub) FetchOrganizations(client *http.Client, organizationsUrl
return logins, nil 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 { var data struct {
Id int `json:"id"` Id int `json:"id"`
......
...@@ -11,7 +11,7 @@ import ( ...@@ -11,7 +11,7 @@ import (
) )
type SocialGoogle struct { type SocialGoogle struct {
*oauth2.Config *SocialBase
allowedDomains []string allowedDomains []string
hostedDomain string hostedDomain string
apiUrl string apiUrl string
...@@ -30,7 +30,7 @@ func (s *SocialGoogle) IsSignupAllowed() bool { ...@@ -30,7 +30,7 @@ func (s *SocialGoogle) IsSignupAllowed() bool {
return s.allowSignup 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 { var data struct {
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"` Email string `json:"email"`
......
...@@ -11,7 +11,7 @@ import ( ...@@ -11,7 +11,7 @@ import (
) )
type SocialGrafanaCom struct { type SocialGrafanaCom struct {
*oauth2.Config *SocialBase
url string url string
allowedOrganizations []string allowedOrganizations []string
allowSignup bool allowSignup bool
...@@ -49,7 +49,7 @@ func (s *SocialGrafanaCom) IsOrganizationMember(organizations []OrgRecord) bool ...@@ -49,7 +49,7 @@ func (s *SocialGrafanaCom) IsOrganizationMember(organizations []OrgRecord) bool
return false 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 { var data struct {
Name string `json:"name"` Name string `json:"name"`
Login string `json:"username"` Login string `json:"username"`
......
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
...@@ -22,7 +23,7 @@ type BasicUserInfo struct { ...@@ -22,7 +23,7 @@ type BasicUserInfo struct {
type SocialConnector interface { type SocialConnector interface {
Type() int Type() int
UserInfo(client *http.Client) (*BasicUserInfo, error) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error)
IsEmailAllowed(email string) bool IsEmailAllowed(email string) bool
IsSignupAllowed() bool IsSignupAllowed() bool
...@@ -31,6 +32,11 @@ type SocialConnector interface { ...@@ -31,6 +32,11 @@ type SocialConnector interface {
Client(ctx context.Context, t *oauth2.Token) *http.Client Client(ctx context.Context, t *oauth2.Token) *http.Client
} }
type SocialBase struct {
*oauth2.Config
log log.Logger
}
type Error struct { type Error struct {
s string s string
} }
...@@ -91,10 +97,15 @@ func NewOAuthService() { ...@@ -91,10 +97,15 @@ func NewOAuthService() {
Scopes: info.Scopes, Scopes: info.Scopes,
} }
logger := log.New("oauth.login." + name)
// GitHub. // GitHub.
if name == "github" { if name == "github" {
SocialMap["github"] = &SocialGithub{ SocialMap["github"] = &SocialGithub{
Config: &config, SocialBase: &SocialBase{
Config: &config,
log: logger,
},
allowedDomains: info.AllowedDomains, allowedDomains: info.AllowedDomains,
apiUrl: info.ApiUrl, apiUrl: info.ApiUrl,
allowSignup: info.AllowSignup, allowSignup: info.AllowSignup,
...@@ -106,7 +117,10 @@ func NewOAuthService() { ...@@ -106,7 +117,10 @@ func NewOAuthService() {
// Google. // Google.
if name == "google" { if name == "google" {
SocialMap["google"] = &SocialGoogle{ SocialMap["google"] = &SocialGoogle{
Config: &config, SocialBase: &SocialBase{
Config: &config,
log: logger,
},
allowedDomains: info.AllowedDomains, allowedDomains: info.AllowedDomains,
hostedDomain: info.HostedDomain, hostedDomain: info.HostedDomain,
apiUrl: info.ApiUrl, apiUrl: info.ApiUrl,
...@@ -116,8 +130,11 @@ func NewOAuthService() { ...@@ -116,8 +130,11 @@ func NewOAuthService() {
// Generic - Uses the same scheme as Github. // Generic - Uses the same scheme as Github.
if name == "generic_oauth" { if name == "generic_oauth" {
SocialMap["generic_oauth"] = &GenericOAuth{ SocialMap["generic_oauth"] = &SocialGenericOAuth{
Config: &config, SocialBase: &SocialBase{
Config: &config,
log: logger,
},
allowedDomains: info.AllowedDomains, allowedDomains: info.AllowedDomains,
apiUrl: info.ApiUrl, apiUrl: info.ApiUrl,
allowSignup: info.AllowSignup, allowSignup: info.AllowSignup,
...@@ -139,7 +156,10 @@ func NewOAuthService() { ...@@ -139,7 +156,10 @@ func NewOAuthService() {
} }
SocialMap["grafana_com"] = &SocialGrafanaCom{ SocialMap["grafana_com"] = &SocialGrafanaCom{
Config: &config, SocialBase: &SocialBase{
Config: &config,
log: logger,
},
url: setting.GrafanaComUrl, url: setting.GrafanaComUrl,
allowSignup: info.AllowSignup, allowSignup: info.AllowSignup,
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()), allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment