login_oauth.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. package api
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "crypto/sha256"
  6. "crypto/tls"
  7. "crypto/x509"
  8. "encoding/base64"
  9. "encoding/hex"
  10. "fmt"
  11. "io/ioutil"
  12. "net/http"
  13. "net/url"
  14. "golang.org/x/oauth2"
  15. "github.com/grafana/grafana/pkg/bus"
  16. "github.com/grafana/grafana/pkg/infra/log"
  17. "github.com/grafana/grafana/pkg/infra/metrics"
  18. "github.com/grafana/grafana/pkg/login"
  19. "github.com/grafana/grafana/pkg/login/social"
  20. m "github.com/grafana/grafana/pkg/models"
  21. "github.com/grafana/grafana/pkg/setting"
  22. )
  23. var (
  24. oauthLogger = log.New("oauth")
  25. OauthStateCookieName = "oauth_state"
  26. )
  27. func GenStateString() string {
  28. rnd := make([]byte, 32)
  29. rand.Read(rnd)
  30. return base64.URLEncoding.EncodeToString(rnd)
  31. }
  32. func (hs *HTTPServer) OAuthLogin(ctx *m.ReqContext) {
  33. if setting.OAuthService == nil {
  34. ctx.Handle(404, "OAuth not enabled", nil)
  35. return
  36. }
  37. name := ctx.Params(":name")
  38. connect, ok := social.SocialMap[name]
  39. if !ok {
  40. ctx.Handle(404, fmt.Sprintf("No OAuth with name %s configured", name), nil)
  41. return
  42. }
  43. errorParam := ctx.Query("error")
  44. if errorParam != "" {
  45. errorDesc := ctx.Query("error_description")
  46. oauthLogger.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc)
  47. hs.redirectWithError(ctx, login.ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
  48. return
  49. }
  50. code := ctx.Query("code")
  51. if code == "" {
  52. state := GenStateString()
  53. hashedState := hashStatecode(state, setting.OAuthService.OAuthInfos[name].ClientSecret)
  54. hs.writeCookie(ctx.Resp, OauthStateCookieName, hashedState, 60)
  55. if setting.OAuthService.OAuthInfos[name].HostedDomain == "" {
  56. ctx.Redirect(connect.AuthCodeURL(state, oauth2.AccessTypeOnline))
  57. } else {
  58. ctx.Redirect(connect.AuthCodeURL(state, oauth2.SetAuthURLParam("hd", setting.OAuthService.OAuthInfos[name].HostedDomain), oauth2.AccessTypeOnline))
  59. }
  60. return
  61. }
  62. cookieState := ctx.GetCookie(OauthStateCookieName)
  63. // delete cookie
  64. ctx.Resp.Header().Del("Set-Cookie")
  65. hs.deleteCookie(ctx.Resp, OauthStateCookieName)
  66. if cookieState == "" {
  67. ctx.Handle(500, "login.OAuthLogin(missing saved state)", nil)
  68. return
  69. }
  70. queryState := hashStatecode(ctx.Query("state"), setting.OAuthService.OAuthInfos[name].ClientSecret)
  71. oauthLogger.Info("state check", "queryState", queryState, "cookieState", cookieState)
  72. if cookieState != queryState {
  73. ctx.Handle(500, "login.OAuthLogin(state mismatch)", nil)
  74. return
  75. }
  76. // handle call back
  77. tr := &http.Transport{
  78. Proxy: http.ProxyFromEnvironment,
  79. TLSClientConfig: &tls.Config{
  80. InsecureSkipVerify: setting.OAuthService.OAuthInfos[name].TlsSkipVerify,
  81. },
  82. }
  83. oauthClient := &http.Client{
  84. Transport: tr,
  85. }
  86. if setting.OAuthService.OAuthInfos[name].TlsClientCert != "" || setting.OAuthService.OAuthInfos[name].TlsClientKey != "" {
  87. cert, err := tls.LoadX509KeyPair(setting.OAuthService.OAuthInfos[name].TlsClientCert, setting.OAuthService.OAuthInfos[name].TlsClientKey)
  88. if err != nil {
  89. ctx.Logger.Error("Failed to setup TlsClientCert", "oauth", name, "error", err)
  90. ctx.Handle(500, "login.OAuthLogin(Failed to setup TlsClientCert)", nil)
  91. return
  92. }
  93. tr.TLSClientConfig.Certificates = append(tr.TLSClientConfig.Certificates, cert)
  94. }
  95. if setting.OAuthService.OAuthInfos[name].TlsClientCa != "" {
  96. caCert, err := ioutil.ReadFile(setting.OAuthService.OAuthInfos[name].TlsClientCa)
  97. if err != nil {
  98. ctx.Logger.Error("Failed to setup TlsClientCa", "oauth", name, "error", err)
  99. ctx.Handle(500, "login.OAuthLogin(Failed to setup TlsClientCa)", nil)
  100. return
  101. }
  102. caCertPool := x509.NewCertPool()
  103. caCertPool.AppendCertsFromPEM(caCert)
  104. tr.TLSClientConfig.RootCAs = caCertPool
  105. }
  106. oauthCtx := context.WithValue(context.Background(), oauth2.HTTPClient, oauthClient)
  107. // get token from provider
  108. token, err := connect.Exchange(oauthCtx, code)
  109. if err != nil {
  110. ctx.Handle(500, "login.OAuthLogin(NewTransportWithCode)", err)
  111. return
  112. }
  113. // token.TokenType was defaulting to "bearer", which is out of spec, so we explicitly set to "Bearer"
  114. token.TokenType = "Bearer"
  115. oauthLogger.Debug("OAuthLogin Got token", "token", token)
  116. // set up oauth2 client
  117. client := connect.Client(oauthCtx, token)
  118. // get user info
  119. userInfo, err := connect.UserInfo(client, token)
  120. if err != nil {
  121. if sErr, ok := err.(*social.Error); ok {
  122. hs.redirectWithError(ctx, sErr)
  123. } else {
  124. ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
  125. }
  126. return
  127. }
  128. oauthLogger.Debug("OAuthLogin got user info", "userInfo", userInfo)
  129. // validate that we got at least an email address
  130. if userInfo.Email == "" {
  131. hs.redirectWithError(ctx, login.ErrNoEmail)
  132. return
  133. }
  134. // validate that the email is allowed to login to grafana
  135. if !connect.IsEmailAllowed(userInfo.Email) {
  136. hs.redirectWithError(ctx, login.ErrEmailNotAllowed)
  137. return
  138. }
  139. extUser := &m.ExternalUserInfo{
  140. AuthModule: "oauth_" + name,
  141. OAuthToken: token,
  142. AuthId: userInfo.Id,
  143. Name: userInfo.Name,
  144. Login: userInfo.Login,
  145. Email: userInfo.Email,
  146. OrgRoles: map[int64]m.RoleType{},
  147. Groups: userInfo.Groups,
  148. }
  149. if userInfo.Role != "" {
  150. extUser.OrgRoles[1] = m.RoleType(userInfo.Role)
  151. }
  152. // add/update user in grafana
  153. cmd := &m.UpsertUserCommand{
  154. ReqContext: ctx,
  155. ExternalUser: extUser,
  156. SignupAllowed: connect.IsSignupAllowed(),
  157. }
  158. err = bus.Dispatch(cmd)
  159. if err != nil {
  160. hs.redirectWithError(ctx, err)
  161. return
  162. }
  163. if cmd.Result.IsDisabled {
  164. hs.redirectWithError(ctx, login.ErrUserDisabled)
  165. return
  166. }
  167. // login
  168. hs.loginUserWithUser(cmd.Result, ctx)
  169. metrics.M_Api_Login_OAuth.Inc()
  170. if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 {
  171. ctx.SetCookie("redirect_to", "", -1, setting.AppSubUrl+"/")
  172. ctx.Redirect(redirectTo)
  173. return
  174. }
  175. ctx.Redirect(setting.AppSubUrl + "/")
  176. }
  177. func (hs *HTTPServer) deleteCookie(w http.ResponseWriter, name string) {
  178. hs.writeCookie(w, name, "", -1)
  179. }
  180. func (hs *HTTPServer) writeCookie(w http.ResponseWriter, name string, value string, maxAge int) {
  181. http.SetCookie(w, &http.Cookie{
  182. Name: name,
  183. MaxAge: maxAge,
  184. Value: value,
  185. HttpOnly: true,
  186. Path: setting.AppSubUrl + "/",
  187. Secure: hs.Cfg.CookieSecure,
  188. SameSite: hs.Cfg.CookieSameSite,
  189. })
  190. }
  191. func hashStatecode(code, seed string) string {
  192. hashBytes := sha256.Sum256([]byte(code + setting.SecretKey + seed))
  193. return hex.EncodeToString(hashBytes[:])
  194. }
  195. func (hs *HTTPServer) redirectWithError(ctx *m.ReqContext, err error, v ...interface{}) {
  196. ctx.Logger.Error(err.Error(), v...)
  197. hs.trySetEncryptedCookie(ctx, LoginErrorCookieName, err.Error(), 60)
  198. ctx.Redirect(setting.AppSubUrl + "/login")
  199. }