|
|
|
|
@ -70,61 +70,61 @@ func genPKCECode() (string, string, error) { |
|
|
|
|
return string(ascii), pkce, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (hs *HTTPServer) OAuthLogin(ctx *contextmodel.ReqContext) { |
|
|
|
|
name := web.Params(ctx.Req)[":name"] |
|
|
|
|
func (hs *HTTPServer) OAuthLogin(reqCtx *contextmodel.ReqContext) { |
|
|
|
|
name := web.Params(reqCtx.Req)[":name"] |
|
|
|
|
loginInfo := loginservice.LoginInfo{AuthModule: name} |
|
|
|
|
|
|
|
|
|
if errorParam := ctx.Query("error"); errorParam != "" { |
|
|
|
|
errorDesc := ctx.Query("error_description") |
|
|
|
|
if errorParam := reqCtx.Query("error"); errorParam != "" { |
|
|
|
|
errorDesc := reqCtx.Query("error_description") |
|
|
|
|
oauthLogger.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc) |
|
|
|
|
hs.handleOAuthLoginErrorWithRedirect(ctx, loginInfo, login.ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc) |
|
|
|
|
hs.handleOAuthLoginErrorWithRedirect(reqCtx, loginInfo, login.ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
code := ctx.Query("code") |
|
|
|
|
code := reqCtx.Query("code") |
|
|
|
|
|
|
|
|
|
if hs.Cfg.AuthBrokerEnabled { |
|
|
|
|
req := &authn.Request{HTTPRequest: ctx.Req, Resp: ctx.Resp} |
|
|
|
|
req := &authn.Request{HTTPRequest: reqCtx.Req, Resp: reqCtx.Resp} |
|
|
|
|
if code == "" { |
|
|
|
|
redirect, err := hs.authnService.RedirectURL(ctx.Req.Context(), authn.ClientWithPrefix(name), req) |
|
|
|
|
redirect, err := hs.authnService.RedirectURL(reqCtx.Req.Context(), authn.ClientWithPrefix(name), req) |
|
|
|
|
if err != nil { |
|
|
|
|
ctx.Redirect(hs.redirectURLWithErrorCookie(ctx, err)) |
|
|
|
|
reqCtx.Redirect(hs.redirectURLWithErrorCookie(reqCtx, err)) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if pkce := redirect.Extra[authn.KeyOAuthPKCE]; pkce != "" { |
|
|
|
|
cookies.WriteCookie(ctx.Resp, OauthPKCECookieName, pkce, hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg) |
|
|
|
|
cookies.WriteCookie(reqCtx.Resp, OauthPKCECookieName, pkce, hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
cookies.WriteCookie(ctx.Resp, OauthStateCookieName, redirect.Extra[authn.KeyOAuthState], hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg) |
|
|
|
|
ctx.Redirect(redirect.URL) |
|
|
|
|
cookies.WriteCookie(reqCtx.Resp, OauthStateCookieName, redirect.Extra[authn.KeyOAuthState], hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg) |
|
|
|
|
reqCtx.Redirect(redirect.URL) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
identity, err := hs.authnService.Login(ctx.Req.Context(), authn.ClientWithPrefix(name), req) |
|
|
|
|
identity, err := hs.authnService.Login(reqCtx.Req.Context(), authn.ClientWithPrefix(name), req) |
|
|
|
|
// NOTE: always delete these cookies, even if login failed
|
|
|
|
|
cookies.DeleteCookie(ctx.Resp, OauthPKCECookieName, hs.CookieOptionsFromCfg) |
|
|
|
|
cookies.DeleteCookie(ctx.Resp, OauthStateCookieName, hs.CookieOptionsFromCfg) |
|
|
|
|
cookies.DeleteCookie(reqCtx.Resp, OauthPKCECookieName, hs.CookieOptionsFromCfg) |
|
|
|
|
cookies.DeleteCookie(reqCtx.Resp, OauthStateCookieName, hs.CookieOptionsFromCfg) |
|
|
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
ctx.Redirect(hs.redirectURLWithErrorCookie(ctx, err)) |
|
|
|
|
reqCtx.Redirect(hs.redirectURLWithErrorCookie(reqCtx, err)) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
metrics.MApiLoginOAuth.Inc() |
|
|
|
|
authn.HandleLoginRedirect(ctx.Req, ctx.Resp, hs.Cfg, identity, hs.ValidateRedirectTo) |
|
|
|
|
authn.HandleLoginRedirect(reqCtx.Req, reqCtx.Resp, hs.Cfg, identity, hs.ValidateRedirectTo) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
provider := hs.SocialService.GetOAuthInfoProvider(name) |
|
|
|
|
if provider == nil { |
|
|
|
|
hs.handleOAuthLoginErrorWithRedirect(ctx, loginInfo, errors.New("OAuth not enabled")) |
|
|
|
|
hs.handleOAuthLoginErrorWithRedirect(reqCtx, loginInfo, errors.New("OAuth not enabled")) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
connect, err := hs.SocialService.GetConnector(name) |
|
|
|
|
if err != nil { |
|
|
|
|
hs.handleOAuthLoginErrorWithRedirect(ctx, loginInfo, fmt.Errorf("no OAuth with name %s configured", name)) |
|
|
|
|
hs.handleOAuthLoginErrorWithRedirect(reqCtx, loginInfo, fmt.Errorf("no OAuth with name %s configured", name)) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -133,15 +133,15 @@ func (hs *HTTPServer) OAuthLogin(ctx *contextmodel.ReqContext) { |
|
|
|
|
if provider.UsePKCE { |
|
|
|
|
ascii, pkce, err := genPKCECode() |
|
|
|
|
if err != nil { |
|
|
|
|
ctx.Logger.Error("Generating PKCE failed", "error", err) |
|
|
|
|
hs.handleOAuthLoginError(ctx, loginInfo, LoginError{ |
|
|
|
|
reqCtx.Logger.Error("Generating PKCE failed", "error", err) |
|
|
|
|
hs.handleOAuthLoginError(reqCtx, loginInfo, LoginError{ |
|
|
|
|
HttpStatus: http.StatusInternalServerError, |
|
|
|
|
PublicMessage: "An internal error occurred", |
|
|
|
|
}) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
cookies.WriteCookie(ctx.Resp, OauthPKCECookieName, ascii, hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg) |
|
|
|
|
cookies.WriteCookie(reqCtx.Resp, OauthPKCECookieName, ascii, hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg) |
|
|
|
|
|
|
|
|
|
opts = append(opts, |
|
|
|
|
oauth2.SetAuthURLParam("code_challenge", pkce), |
|
|
|
|
@ -151,8 +151,8 @@ func (hs *HTTPServer) OAuthLogin(ctx *contextmodel.ReqContext) { |
|
|
|
|
|
|
|
|
|
state, err := GenStateString() |
|
|
|
|
if err != nil { |
|
|
|
|
ctx.Logger.Error("Generating state string failed", "err", err) |
|
|
|
|
hs.handleOAuthLoginError(ctx, loginInfo, LoginError{ |
|
|
|
|
reqCtx.Logger.Error("Generating state string failed", "err", err) |
|
|
|
|
hs.handleOAuthLoginError(reqCtx, loginInfo, LoginError{ |
|
|
|
|
HttpStatus: http.StatusInternalServerError, |
|
|
|
|
PublicMessage: "An internal error occurred", |
|
|
|
|
}) |
|
|
|
|
@ -160,32 +160,32 @@ func (hs *HTTPServer) OAuthLogin(ctx *contextmodel.ReqContext) { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
hashedState := hs.hashStatecode(state, provider.ClientSecret) |
|
|
|
|
cookies.WriteCookie(ctx.Resp, OauthStateCookieName, hashedState, hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg) |
|
|
|
|
cookies.WriteCookie(reqCtx.Resp, OauthStateCookieName, hashedState, hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg) |
|
|
|
|
if provider.HostedDomain != "" { |
|
|
|
|
opts = append(opts, oauth2.SetAuthURLParam("hd", provider.HostedDomain)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
ctx.Redirect(connect.AuthCodeURL(state, opts...)) |
|
|
|
|
reqCtx.Redirect(connect.AuthCodeURL(state, opts...)) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
cookieState := ctx.GetCookie(OauthStateCookieName) |
|
|
|
|
cookieState := reqCtx.GetCookie(OauthStateCookieName) |
|
|
|
|
|
|
|
|
|
// delete cookie
|
|
|
|
|
cookies.DeleteCookie(ctx.Resp, OauthStateCookieName, hs.CookieOptionsFromCfg) |
|
|
|
|
cookies.DeleteCookie(reqCtx.Resp, OauthStateCookieName, hs.CookieOptionsFromCfg) |
|
|
|
|
|
|
|
|
|
if cookieState == "" { |
|
|
|
|
hs.handleOAuthLoginError(ctx, loginInfo, LoginError{ |
|
|
|
|
hs.handleOAuthLoginError(reqCtx, loginInfo, LoginError{ |
|
|
|
|
HttpStatus: http.StatusInternalServerError, |
|
|
|
|
PublicMessage: "login.OAuthLogin(missing saved state)", |
|
|
|
|
}) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
queryState := hs.hashStatecode(ctx.Query("state"), provider.ClientSecret) |
|
|
|
|
queryState := hs.hashStatecode(reqCtx.Query("state"), provider.ClientSecret) |
|
|
|
|
oauthLogger.Info("state check", "queryState", queryState, "cookieState", cookieState) |
|
|
|
|
if cookieState != queryState { |
|
|
|
|
hs.handleOAuthLoginError(ctx, loginInfo, LoginError{ |
|
|
|
|
hs.handleOAuthLoginError(reqCtx, loginInfo, LoginError{ |
|
|
|
|
HttpStatus: http.StatusInternalServerError, |
|
|
|
|
PublicMessage: "login.OAuthLogin(state mismatch)", |
|
|
|
|
}) |
|
|
|
|
@ -194,19 +194,20 @@ func (hs *HTTPServer) OAuthLogin(ctx *contextmodel.ReqContext) { |
|
|
|
|
|
|
|
|
|
oauthClient, err := hs.SocialService.GetOAuthHttpClient(name) |
|
|
|
|
if err != nil { |
|
|
|
|
ctx.Logger.Error("Failed to create OAuth http client", "error", err) |
|
|
|
|
hs.handleOAuthLoginError(ctx, loginInfo, LoginError{ |
|
|
|
|
reqCtx.Logger.Error("Failed to create OAuth http client", "error", err) |
|
|
|
|
hs.handleOAuthLoginError(reqCtx, loginInfo, LoginError{ |
|
|
|
|
HttpStatus: http.StatusInternalServerError, |
|
|
|
|
PublicMessage: "login.OAuthLogin(" + err.Error() + ")", |
|
|
|
|
}) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
oauthCtx := context.WithValue(context.Background(), oauth2.HTTPClient, oauthClient) |
|
|
|
|
ctx := reqCtx.Req.Context() |
|
|
|
|
oauthCtx := context.WithValue(ctx, oauth2.HTTPClient, oauthClient) |
|
|
|
|
opts := []oauth2.AuthCodeOption{} |
|
|
|
|
|
|
|
|
|
codeVerifier := ctx.GetCookie(OauthPKCECookieName) |
|
|
|
|
cookies.DeleteCookie(ctx.Resp, OauthPKCECookieName, hs.CookieOptionsFromCfg) |
|
|
|
|
codeVerifier := reqCtx.GetCookie(OauthPKCECookieName) |
|
|
|
|
cookies.DeleteCookie(reqCtx.Resp, OauthPKCECookieName, hs.CookieOptionsFromCfg) |
|
|
|
|
if codeVerifier != "" { |
|
|
|
|
opts = append(opts, |
|
|
|
|
oauth2.SetAuthURLParam("code_verifier", codeVerifier), |
|
|
|
|
@ -216,7 +217,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *contextmodel.ReqContext) { |
|
|
|
|
// get token from provider
|
|
|
|
|
token, err := connect.Exchange(oauthCtx, code, opts...) |
|
|
|
|
if err != nil { |
|
|
|
|
hs.handleOAuthLoginError(ctx, loginInfo, LoginError{ |
|
|
|
|
hs.handleOAuthLoginError(reqCtx, loginInfo, LoginError{ |
|
|
|
|
HttpStatus: http.StatusInternalServerError, |
|
|
|
|
PublicMessage: "login.OAuthLogin(NewTransportWithCode)", |
|
|
|
|
Err: err, |
|
|
|
|
@ -245,13 +246,13 @@ func (hs *HTTPServer) OAuthLogin(ctx *contextmodel.ReqContext) { |
|
|
|
|
client := connect.Client(oauthCtx, token) |
|
|
|
|
|
|
|
|
|
// get user info
|
|
|
|
|
userInfo, err := connect.UserInfo(client, token) |
|
|
|
|
userInfo, err := connect.UserInfo(ctx, client, token) |
|
|
|
|
if err != nil { |
|
|
|
|
var sErr *social.Error |
|
|
|
|
if errors.As(err, &sErr) { |
|
|
|
|
hs.handleOAuthLoginErrorWithRedirect(ctx, loginInfo, sErr) |
|
|
|
|
hs.handleOAuthLoginErrorWithRedirect(reqCtx, loginInfo, sErr) |
|
|
|
|
} else { |
|
|
|
|
hs.handleOAuthLoginError(ctx, loginInfo, LoginError{ |
|
|
|
|
hs.handleOAuthLoginError(reqCtx, loginInfo, LoginError{ |
|
|
|
|
HttpStatus: http.StatusInternalServerError, |
|
|
|
|
PublicMessage: fmt.Sprintf("login.OAuthLogin(get info from %s)", name), |
|
|
|
|
Err: err, |
|
|
|
|
@ -264,34 +265,34 @@ func (hs *HTTPServer) OAuthLogin(ctx *contextmodel.ReqContext) { |
|
|
|
|
|
|
|
|
|
// validate that we got at least an email address
|
|
|
|
|
if userInfo.Email == "" { |
|
|
|
|
hs.handleOAuthLoginErrorWithRedirect(ctx, loginInfo, login.ErrNoEmail) |
|
|
|
|
hs.handleOAuthLoginErrorWithRedirect(reqCtx, loginInfo, login.ErrNoEmail) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// validate that the email is allowed to login to grafana
|
|
|
|
|
if !connect.IsEmailAllowed(userInfo.Email) { |
|
|
|
|
hs.handleOAuthLoginErrorWithRedirect(ctx, loginInfo, login.ErrEmailNotAllowed) |
|
|
|
|
hs.handleOAuthLoginErrorWithRedirect(reqCtx, loginInfo, login.ErrEmailNotAllowed) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
loginInfo.ExternalUser = *hs.buildExternalUserInfo(token, userInfo, name) |
|
|
|
|
loginInfo.User, err = hs.SyncUser(ctx, &loginInfo.ExternalUser, connect) |
|
|
|
|
loginInfo.User, err = hs.SyncUser(reqCtx, &loginInfo.ExternalUser, connect) |
|
|
|
|
if err != nil { |
|
|
|
|
hs.handleOAuthLoginErrorWithRedirect(ctx, loginInfo, err) |
|
|
|
|
hs.handleOAuthLoginErrorWithRedirect(reqCtx, loginInfo, err) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// login
|
|
|
|
|
if err := hs.loginUserWithUser(loginInfo.User, ctx); err != nil { |
|
|
|
|
hs.handleOAuthLoginErrorWithRedirect(ctx, loginInfo, err) |
|
|
|
|
if err := hs.loginUserWithUser(loginInfo.User, reqCtx); err != nil { |
|
|
|
|
hs.handleOAuthLoginErrorWithRedirect(reqCtx, loginInfo, err) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
loginInfo.HTTPStatus = http.StatusOK |
|
|
|
|
hs.HooksService.RunLoginHook(&loginInfo, ctx) |
|
|
|
|
hs.HooksService.RunLoginHook(&loginInfo, reqCtx) |
|
|
|
|
metrics.MApiLoginOAuth.Inc() |
|
|
|
|
|
|
|
|
|
ctx.Redirect(hs.GetRedirectURL(ctx)) |
|
|
|
|
reqCtx.Redirect(hs.GetRedirectURL(reqCtx)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// buildExternalUserInfo returns a ExternalUserInfo struct from OAuth user profile
|
|
|
|
|
|