diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index ae6b42d41b06..8b4e494d6eca 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -226,7 +226,7 @@ }, { "ImportPath": "github.com/RangelReale/osincli", - "Rev": "325fd800be65f8133ddbe73aef3cbe6780266a47" + "Rev": "23618ea0fc3faa3f43954ce8ff48e31f5c784212" }, { "ImportPath": "github.com/coreos/etcd/config", diff --git a/Godeps/_workspace/src/github.com/RangelReale/osincli/util.go b/Godeps/_workspace/src/github.com/RangelReale/osincli/util.go index 414844875db6..3aa6bc8f7e52 100644 --- a/Godeps/_workspace/src/github.com/RangelReale/osincli/util.go +++ b/Godeps/_workspace/src/github.com/RangelReale/osincli/util.go @@ -45,6 +45,9 @@ func downloadData(method string, u *url.URL, auth *BasicAuth, transport http.Rou preq.Header.Set("Content-Type", contenttype) } + // Explicitly set accept header to JSON + preq.Header.Set("Accept", "application/json") + // do request client := &http.Client{} if transport != nil { diff --git a/pkg/auth/api/types.go b/pkg/auth/api/types.go index 6afbc0114484..639fe7893feb 100644 --- a/pkg/auth/api/types.go +++ b/pkg/auth/api/types.go @@ -1,5 +1,6 @@ package api +// TODO: Add display name to common meta? type UserInfo interface { GetName() string GetUID() string diff --git a/pkg/auth/authenticator/file/token.go b/pkg/auth/authenticator/file/token.go index 4f8b1fa820b5..f8a84d5cbdaa 100644 --- a/pkg/auth/authenticator/file/token.go +++ b/pkg/auth/authenticator/file/token.go @@ -9,6 +9,7 @@ import ( ) type TokenAuthenticator struct { + path string tokens map[string]*api.DefaultUserInfo } @@ -43,6 +44,7 @@ func NewTokenAuthenticator(path string) (*TokenAuthenticator, error) { } return &TokenAuthenticator{ + path: file.Name(), tokens: tokens, }, nil } diff --git a/pkg/auth/handlers/authrequest.go b/pkg/auth/handlers/authrequest.go index 37b087520aae..c0dd67b2537b 100644 --- a/pkg/auth/handlers/authrequest.go +++ b/pkg/auth/handlers/authrequest.go @@ -3,6 +3,7 @@ package handlers import ( "net/http" + "github.com/golang/glog" "github.com/openshift/origin/pkg/auth/authenticator" ) @@ -17,6 +18,9 @@ func NewRequestAuthenticator(context RequestContext, auth authenticator.Request, if err != nil || !ok { failed.ServeHTTP(w, req) return + } else { + glog.V(1).Infof("Found user, %v, when accessing %v", user, req.URL) + } context.Set(req, user) diff --git a/pkg/auth/oauth/external/github/github.go b/pkg/auth/oauth/external/github/github.go new file mode 100644 index 000000000000..85d969ab4674 --- /dev/null +++ b/pkg/auth/oauth/external/github/github.go @@ -0,0 +1,86 @@ +package github + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/RangelReale/osincli" + "github.com/openshift/origin/pkg/auth/api" + "github.com/openshift/origin/pkg/auth/oauth/external" +) + +const ( + githubAuthorizeUrl = "https://github.com/login/oauth/authorize" + githubTokenUrl = "https://github.com/login/oauth/access_token" + githubUserApiUrl = "https://api.github.com/user" + githubOauthScope = "user:email" +) + +type provider struct { + client_id, client_secret string +} + +type githubUser struct { + ID uint64 + Login string + Email string + Name string +} + +func NewProvider(client_id, client_secret string) external.Provider { + return provider{client_id, client_secret} +} + +func (p provider) NewConfig() (*osincli.ClientConfig, error) { + config := &osincli.ClientConfig{ + ClientId: p.client_id, + ClientSecret: p.client_secret, + ErrorsInStatusCode: true, + SendClientSecretInParams: true, + AuthorizeUrl: githubAuthorizeUrl, + TokenUrl: githubTokenUrl, + Scope: githubOauthScope, + } + return config, nil +} + +func (p provider) AddCustomParameters(req *osincli.AuthorizeRequest) { +} + +func (p provider) GetUserInfo(data *osincli.AccessData) (api.UserInfo, bool, error) { + + req, _ := http.NewRequest("GET", githubUserApiUrl, nil) + req.Header.Set("Authorization", fmt.Sprintf("bearer %s", data.AccessToken)) + + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, false, err + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, false, err + } + + userdata := githubUser{} + err = json.Unmarshal(body, &userdata) + if err != nil { + return nil, false, err + } + + if userdata.ID == 0 { + return nil, false, fmt.Errorf("Could not retrieve GitHub id") + } + + user := &api.DefaultUserInfo{ + Name: fmt.Sprintf("%d", userdata.ID), + Extra: map[string]string{ + "name": userdata.Name, + "login": userdata.Login, + "email": userdata.Email, + }, + } + return user, true, nil +} diff --git a/pkg/auth/oauth/external/google/google.go b/pkg/auth/oauth/external/google/google.go new file mode 100644 index 000000000000..85aa628cdd96 --- /dev/null +++ b/pkg/auth/oauth/external/google/google.go @@ -0,0 +1,106 @@ +package google + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "strings" + + "github.com/RangelReale/osincli" + "github.com/golang/glog" + "github.com/openshift/origin/pkg/auth/api" + "github.com/openshift/origin/pkg/auth/oauth/external" +) + +const ( + googleAuthorizeUrl = "https://accounts.google.com/o/oauth2/auth" + googleTokenUrl = "https://accounts.google.com/o/oauth2/token" + googleOauthScope = "profile email" +) + +type provider struct { + client_id, client_secret string +} + +func NewProvider(client_id, client_secret string) external.Provider { + return provider{client_id, client_secret} +} + +func (p provider) NewConfig() (*osincli.ClientConfig, error) { + config := &osincli.ClientConfig{ + ClientId: p.client_id, + ClientSecret: p.client_secret, + ErrorsInStatusCode: true, + SendClientSecretInParams: true, + AuthorizeUrl: googleAuthorizeUrl, + TokenUrl: googleTokenUrl, + Scope: googleOauthScope, + } + return config, nil +} + +func (p provider) AddCustomParameters(req *osincli.AuthorizeRequest) { + req.CustomParameters["include_granted_scopes"] = "true" + req.CustomParameters["access_type"] = "offline" +} + +func (p provider) GetUserInfo(data *osincli.AccessData) (api.UserInfo, bool, error) { + idToken, ok := data.ResponseData["id_token"].(string) + if !ok { + return nil, false, fmt.Errorf("No id_token returned in %v", data.ResponseData) + } + + userdata, err := decodeJWT(idToken) + if err != nil { + return nil, false, err + } + + id, _ := userdata["id"].(string) + email, _ := userdata["email"].(string) + if id == "" || email == "" { + return nil, false, fmt.Errorf("Could not retrieve Google id") + } + + user := &api.DefaultUserInfo{ + Name: id, + Extra: map[string]string{ + "name": email, + "email": email, + }, + } + return user, true, nil +} + +// Decode JWT +// http://openid.net/specs/draft-jones-json-web-token-07.html +func decodeJWT(jwt string) (map[string]interface{}, error) { + jwtParts := strings.Split(jwt, ".") + if len(jwtParts) != 3 { + return nil, fmt.Errorf("Invalid JSON Web Token: expected 3 parts, got %d", len(jwtParts)) + } + + encodedPayload := jwtParts[1] + glog.V(4).Infof("got encodedPayload") + + // Re-pad, if needed + if l := len(encodedPayload) % 4; l != 0 { + padding := strings.Repeat("=", 4-l) + encodedPayload += padding + glog.V(4).Infof("added padding: %s\n", padding) + } + + decodedPayload, err := base64.StdEncoding.DecodeString(encodedPayload) + if err != nil { + return nil, fmt.Errorf("Error decoding payload: %v\n", err) + } + glog.V(4).Infof("got decodedPayload") + + var data map[string]interface{} + err = json.Unmarshal([]byte(decodedPayload), &data) + if err != nil { + return nil, fmt.Errorf("Error parsing token: %v\n", err) + } + glog.V(4).Infof("got id_token data") + + return data, nil +} diff --git a/pkg/auth/oauth/external/handler.go b/pkg/auth/oauth/external/handler.go new file mode 100644 index 000000000000..dbc110cf6389 --- /dev/null +++ b/pkg/auth/oauth/external/handler.go @@ -0,0 +1,173 @@ +package external + +import ( + "fmt" + "net/http" + "net/url" + + "github.com/RangelReale/osincli" + "github.com/golang/glog" + "github.com/openshift/origin/pkg/auth/api" + "github.com/openshift/origin/pkg/auth/oauth/handlers" +) + +type Handler struct { + provider Provider + state State + clientConfig *osincli.ClientConfig + client *osincli.Client + success handlers.AuthenticationSuccessHandler + error handlers.AuthenticationErrorHandler +} + +func NewHandler(provider Provider, state State, redirectUrl string, success handlers.AuthenticationSuccessHandler, error handlers.AuthenticationErrorHandler) (*Handler, error) { + clientConfig, err := provider.NewConfig() + if err != nil { + return nil, err + } + + clientConfig.RedirectUrl = redirectUrl + + client, err := osincli.NewClient(clientConfig) + if err != nil { + return nil, err + } + + return &Handler{ + provider: provider, + state: state, + clientConfig: clientConfig, + client: client, + success: success, + error: error, + }, nil +} + +// Implements oauth.handlers.AuthenticationHandler +func (h *Handler) AuthenticationNeeded(w http.ResponseWriter, req *http.Request) { + glog.V(4).Infof("Authentication needed for %v", h) + + authReq := h.client.NewAuthorizeRequest(osincli.CODE) + h.provider.AddCustomParameters(authReq) + + state, err := h.state.Generate(w, req) + if err != nil { + glog.V(4).Infof("Error generating state: %v", err) + h.AuthenticationError(err, w, req) + return + } + + oauthUrl := authReq.GetAuthorizeUrlWithParams(state) + glog.V(4).Infof("redirect to %v", oauthUrl) + + http.Redirect(w, req, oauthUrl.String(), http.StatusFound) +} + +// Implements oauth.handlers.AuthenticationHandler +func (h *Handler) AuthenticationError(err error, w http.ResponseWriter, req *http.Request) { + h.error.AuthenticationError(err, w, req) +} + +// Handles the callback request in response to an external oauth flow +func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + + // Extract auth code + authReq := h.client.NewAuthorizeRequest(osincli.CODE) + authData, err := authReq.HandleRequest(req) + if err != nil { + glog.V(4).Infof("Error handling request: %v", err) + h.AuthenticationError(err, w, req) + return + } + + glog.V(4).Infof("Got auth data") + + // Exchange code for a token + accessReq := h.client.NewAccessRequest(osincli.AUTHORIZATION_CODE, authData) + accessData, err := accessReq.GetToken() + if err != nil { + glog.V(4).Infof("Error getting access token:", err) + h.AuthenticationError(err, w, req) + return + } + + glog.V(4).Infof("Got access data") + + user, ok, err := h.provider.GetUserInfo(accessData) + if err != nil { + glog.V(4).Infof("Error getting user info: %v", err) + h.AuthenticationError(err, w, req) + return + } + if !ok || user == nil { + glog.V(4).Infof("Could not get user info from access token") + h.AuthenticationError(fmt.Errorf("Could not get user info from access token"), w, req) + return + } + + glog.V(4).Infof("Got user data: %#v", user) + + ok, err = h.state.Check(authData.State, w, req) + if !ok { + glog.V(4).Infof("State is invalid") + h.AuthenticationError(fmt.Errorf("State is invalid"), w, req) + return + } + if err != nil { + glog.V(4).Infof("Error verifying state: %v", err) + h.AuthenticationError(err, w, req) + return + } + + err = h.success.AuthenticationSucceeded(user, authData.State, w, req) + if err != nil { + glog.V(4).Infof("Error calling success handler: %v", err) + h.AuthenticationError(err, w, req) + return + } +} + +// Provides default state-building, validation, and parsing to contain CSRF and "then" redirection +type defaultState struct{} + +func DefaultState() State { + return defaultState{} +} +func (defaultState) Generate(w http.ResponseWriter, req *http.Request) (string, error) { + state := url.Values{ + "csrf": {"..."}, // TODO: get csrf + "then": {req.URL.String()}, + } + return state.Encode(), nil +} +func (defaultState) Check(state string, w http.ResponseWriter, req *http.Request) (bool, error) { + values, err := url.ParseQuery(state) + if err != nil { + return false, err + } + csrf := values.Get("csrf") + if csrf != "..." { + return false, fmt.Errorf("State did not contain valid CSRF token (expected %s, got %s)", "...", csrf) + } + + then := values.Get("then") + if then == "" { + return false, fmt.Errorf("State did not contain a redirect") + } + + return true, nil +} +func (defaultState) AuthenticationSucceeded(user api.UserInfo, state string, w http.ResponseWriter, req *http.Request) error { + values, err := url.ParseQuery(state) + if err != nil { + return err + } + + then := values.Get("then") + if len(then) == 0 { + return fmt.Errorf("No redirect given") + } + + http.Redirect(w, req, then, http.StatusFound) + return nil +} diff --git a/pkg/auth/oauth/external/interfaces.go b/pkg/auth/oauth/external/interfaces.go new file mode 100644 index 000000000000..dd2edfbd7e2a --- /dev/null +++ b/pkg/auth/oauth/external/interfaces.go @@ -0,0 +1,27 @@ +/* +Package external implements an OAuth flow with an external identity provider +*/ + +package external + +import ( + "net/http" + + "github.com/RangelReale/osincli" + "github.com/openshift/origin/pkg/auth/api" +) + +// Provider encapsulates the URLs, configuration, any custom authorize request parameters, and +// the method for transforming an access token into an identity, for an external OAuth provider. +type Provider interface { + NewConfig() (*osincli.ClientConfig, error) + AddCustomParameters(*osincli.AuthorizeRequest) + GetUserInfo(*osincli.AccessData) (api.UserInfo, bool, error) +} + +// State handles generating and verifying the state parameter round-tripped to an external OAuth flow. +// Examples: CSRF protection, post authentication redirection +type State interface { + Generate(w http.ResponseWriter, req *http.Request) (string, error) + Check(state string, w http.ResponseWriter, req *http.Request) (bool, error) +} diff --git a/pkg/auth/oauth/handlers/grant.go b/pkg/auth/oauth/handlers/grant.go index 692f117b2e1d..91a75301f976 100644 --- a/pkg/auth/oauth/handlers/grant.go +++ b/pkg/auth/oauth/handlers/grant.go @@ -42,7 +42,7 @@ func (h *GrantCheck) HandleAuthorize(ar *osin.AuthorizeRequest, w http.ResponseW return true } if !ok { - h.handler.GrantNeeded(grant, w, req) + h.handler.GrantNeeded(ar.Client, user, grant, w, req) return true } diff --git a/pkg/auth/oauth/handlers/interfaces.go b/pkg/auth/oauth/handlers/interfaces.go index ed9395f4958b..5c87ae8176a0 100644 --- a/pkg/auth/oauth/handlers/interfaces.go +++ b/pkg/auth/oauth/handlers/interfaces.go @@ -11,11 +11,32 @@ type AuthenticationHandler interface { AuthenticationError(err error, w http.ResponseWriter, req *http.Request) } +type AuthenticationSuccessHandler interface { + AuthenticationSucceeded(user api.UserInfo, state string, w http.ResponseWriter, req *http.Request) error +} + +type AuthenticationErrorHandler interface { + AuthenticationError(err error, w http.ResponseWriter, req *http.Request) +} + type GrantChecker interface { HasAuthorizedClient(client api.Client, user api.UserInfo, grant *api.Grant) (bool, error) } type GrantHandler interface { - GrantNeeded(grant *api.Grant, w http.ResponseWriter, req *http.Request) + GrantNeeded(client api.Client, user api.UserInfo, grant *api.Grant, w http.ResponseWriter, req *http.Request) GrantError(err error, w http.ResponseWriter, req *http.Request) } + +// AuthenticationSuccessHandlers combines multiple AuthenticationSuccessHandler objects into a chain. +// On success, each handler is called. If any handler returns an error, the chain is aborted. +type AuthenticationSuccessHandlers []AuthenticationSuccessHandler + +func (all AuthenticationSuccessHandlers) AuthenticationSucceeded(user api.UserInfo, state string, w http.ResponseWriter, req *http.Request) error { + for _, h := range all { + if err := h.AuthenticationSucceeded(user, state, w, req); err != nil { + return err + } + } + return nil +} diff --git a/pkg/auth/oauth/registry/registry_test.go b/pkg/auth/oauth/registry/registry_test.go index c37d7ffca717..24d2f4d8d876 100644 --- a/pkg/auth/oauth/registry/registry_test.go +++ b/pkg/auth/oauth/registry/registry_test.go @@ -38,7 +38,7 @@ func (h *testHandlers) AuthenticateRequest(req *http.Request) (api.UserInfo, boo return h.User, h.Authenticate, h.Err } -func (h *testHandlers) GrantNeeded(grant *api.Grant, w http.ResponseWriter, req *http.Request) { +func (h *testHandlers) GrantNeeded(client api.Client, user api.UserInfo, grant *api.Grant, w http.ResponseWriter, req *http.Request) { h.GrantNeed = true } diff --git a/pkg/auth/server/login/implicit.go b/pkg/auth/server/login/implicit.go index f2af32d3a354..a69fc3b092b2 100644 --- a/pkg/auth/server/login/implicit.go +++ b/pkg/auth/server/login/implicit.go @@ -8,11 +8,12 @@ import ( "github.com/openshift/origin/pkg/auth/api" "github.com/openshift/origin/pkg/auth/authenticator" + "github.com/openshift/origin/pkg/auth/oauth/handlers" ) type RequestAuthenticator interface { authenticator.Request - AuthenticationSucceeded(user api.UserInfo, then string, w http.ResponseWriter, req *http.Request) + handlers.AuthenticationSuccessHandler } type ConfirmFormRenderer interface { diff --git a/pkg/auth/server/login/implicit_test.go b/pkg/auth/server/login/implicit_test.go index 61b84951df3e..4981a78596e8 100644 --- a/pkg/auth/server/login/implicit_test.go +++ b/pkg/auth/server/login/implicit_test.go @@ -26,10 +26,11 @@ func (t *testImplicit) AuthenticateRequest(req *http.Request) (api.UserInfo, boo return t.User, t.Success, t.Err } -func (t *testImplicit) AuthenticationSucceeded(user api.UserInfo, then string, w http.ResponseWriter, req *http.Request) { +func (t *testImplicit) AuthenticationSucceeded(user api.UserInfo, then string, w http.ResponseWriter, req *http.Request) error { t.Called = true t.User = user t.Then = then + return nil } func TestImplicit(t *testing.T) { diff --git a/pkg/auth/server/login/login.go b/pkg/auth/server/login/login.go index d17cf852aeb0..eada46ebe6cf 100644 --- a/pkg/auth/server/login/login.go +++ b/pkg/auth/server/login/login.go @@ -7,13 +7,13 @@ import ( "github.com/golang/glog" - "github.com/openshift/origin/pkg/auth/api" "github.com/openshift/origin/pkg/auth/authenticator" + "github.com/openshift/origin/pkg/auth/oauth/handlers" ) type PasswordAuthenticator interface { authenticator.Password - AuthenticationSucceeded(context api.UserInfo, then string, w http.ResponseWriter, req *http.Request) + handlers.AuthenticationSuccessHandler } type LoginFormRenderer interface { diff --git a/pkg/auth/server/login/login_test.go b/pkg/auth/server/login/login_test.go index af1bf9c74e75..34061a921e9b 100644 --- a/pkg/auth/server/login/login_test.go +++ b/pkg/auth/server/login/login_test.go @@ -41,10 +41,11 @@ func (t *testAuth) AuthenticatePassword(user, password string) (api.UserInfo, bo return t.User, t.Success, t.Err } -func (t *testAuth) AuthenticationSucceeded(user api.UserInfo, then string, w http.ResponseWriter, req *http.Request) { +func (t *testAuth) AuthenticationSucceeded(user api.UserInfo, then string, w http.ResponseWriter, req *http.Request) error { t.Called = true t.User = user t.Then = then + return nil } func TestLogin(t *testing.T) { diff --git a/pkg/auth/server/session/authenticator.go b/pkg/auth/server/session/authenticator.go index ea572f45aab9..1f32d2bab045 100644 --- a/pkg/auth/server/session/authenticator.go +++ b/pkg/auth/server/session/authenticator.go @@ -43,7 +43,7 @@ func (a *SessionAuthenticator) AuthenticateRequest(req *http.Request) (api.UserI }, true, nil } -func (a *SessionAuthenticator) AuthenticationSucceeded(user api.UserInfo, w http.ResponseWriter, req *http.Request) error { +func (a *SessionAuthenticator) AuthenticationSucceeded(user api.UserInfo, state string, w http.ResponseWriter, req *http.Request) error { session, err := a.store.Get(req, a.name) if err != nil { return err @@ -52,3 +52,12 @@ func (a *SessionAuthenticator) AuthenticationSucceeded(user api.UserInfo, w http values[UserNameKey] = user.GetName() return a.store.Save(w, req) } + +func (a *SessionAuthenticator) InvalidateAuthentication(context api.UserInfo, w http.ResponseWriter, req *http.Request) error { + session, err := a.store.Get(req, a.name) + if err != nil { + return err + } + session.Values()[UserNameKey] = "" + return a.store.Save(w, req) +} diff --git a/pkg/cmd/server/origin/auth.go b/pkg/cmd/server/origin/auth.go index 753197df2867..c4136f9a3fa0 100644 --- a/pkg/cmd/server/origin/auth.go +++ b/pkg/cmd/server/origin/auth.go @@ -1,18 +1,28 @@ package origin import ( + "errors" "fmt" "net/http" "net/url" "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" + "github.com/golang/glog" "github.com/openshift/origin/pkg/auth/api" "github.com/openshift/origin/pkg/auth/authenticator" + "github.com/openshift/origin/pkg/auth/authenticator/bearertoken" + authfile "github.com/openshift/origin/pkg/auth/authenticator/file" + "github.com/openshift/origin/pkg/auth/authenticator/requestheader" + "github.com/openshift/origin/pkg/auth/oauth/external" + "github.com/openshift/origin/pkg/auth/oauth/external/github" + "github.com/openshift/origin/pkg/auth/oauth/external/google" "github.com/openshift/origin/pkg/auth/oauth/handlers" "github.com/openshift/origin/pkg/auth/oauth/registry" + authnregistry "github.com/openshift/origin/pkg/auth/oauth/registry" "github.com/openshift/origin/pkg/auth/server/login" "github.com/openshift/origin/pkg/auth/server/session" + cmdutil "github.com/openshift/origin/pkg/cmd/util" oauthetcd "github.com/openshift/origin/pkg/oauth/registry/etcd" "github.com/openshift/origin/pkg/oauth/server/osinserver" @@ -20,11 +30,13 @@ import ( ) const ( - OpenShiftOAuthAPIPrefix = "/oauth" - OpenShiftLoginPrefix = "/login" + OpenShiftOAuthAPIPrefix = "/oauth" + OpenShiftLoginPrefix = "/login" + OpenShiftOAuthCallbackPrefix = "/oauth2callback" ) type AuthConfig struct { + MasterAddr string SessionSecrets []string EtcdHelper tools.EtcdHelper } @@ -35,22 +47,33 @@ type AuthConfig struct { // a single string value). func (c *AuthConfig) InstallAPI(mux cmdutil.Mux) []string { oauthEtcd := oauthetcd.New(c.EtcdHelper) + + authRequestHandler := c.getAuthenticationRequestHandler() + + // Check if the authentication handler wants to be told when we authenticated + success, ok := authRequestHandler.(handlers.AuthenticationSuccessHandler) + if !ok { + success = emptySuccess{} + } + authHandler := c.getAuthenticationHandler(mux, success, emptyError{}) + storage := registrystorage.New(oauthEtcd, oauthEtcd, oauthEtcd, registry.NewUserConversion()) config := osinserver.NewDefaultServerConfig() - sessionStore := session.NewStore(c.SessionSecrets...) - sessionAuth := session.NewSessionAuthenticator(sessionStore, "ssn") + + grantChecker := registry.NewClientAuthorizationGrantChecker(oauthEtcd) + grantHandler := emptyGrant{} server := osinserver.New( config, storage, osinserver.AuthorizeHandlers{ handlers.NewAuthorizeAuthenticator( - &redirectAuthHandler{RedirectURL: OpenShiftLoginPrefix, ThenParam: "then"}, - sessionAuth, + authHandler, + authRequestHandler, ), handlers.NewGrantCheck( - registry.NewClientAuthorizationGrantChecker(oauthEtcd), - emptyGrant{}, + grantChecker, + grantHandler, ), }, osinserver.AccessHandlers{ @@ -58,9 +81,11 @@ func (c *AuthConfig) InstallAPI(mux cmdutil.Mux) []string { }, ) server.Install(mux, OpenShiftOAuthAPIPrefix) - - login := login.NewLogin(emptyCsrf{}, &sessionPasswordAuthenticator{emptyPasswordAuth{}, sessionAuth}, login.DefaultLoginFormRenderer) - login.Install(mux, OpenShiftLoginPrefix) + // glog.Infof("oauth server configured as: %#v", server) + // glog.Infof("auth handler: %#v", authHandler) + // glog.Infof("auth request handler: %#v", authRequestHandler) + // glog.Infof("grant checker: %#v", grantChecker) + // glog.Infof("grant handler: %#v", grantHandler) return []string{ fmt.Sprintf("Started OAuth2 API at %%s%s", OpenShiftOAuthAPIPrefix), @@ -68,6 +93,84 @@ func (c *AuthConfig) InstallAPI(mux cmdutil.Mux) []string { } } +func (c *AuthConfig) getAuthenticationHandler(mux cmdutil.Mux, success handlers.AuthenticationSuccessHandler, error handlers.AuthenticationErrorHandler) handlers.AuthenticationHandler { + // TODO presumeably we'll want either a list of what we've got or a way to describe a registry of these + // hard-coded strings as a stand-in until it gets sorted out + var authHandler handlers.AuthenticationHandler + authHandlerType := env("ORIGIN_AUTH_HANDLER", "empty") + switch authHandlerType { + case "google", "github": + callbackPath := OpenShiftOAuthCallbackPrefix + "/" + authHandlerType + + var oauthProvider external.Provider + if authHandlerType == "google" { + oauthProvider = google.NewProvider(env("ORIGIN_GOOGLE_CLIENT_ID", ""), env("ORIGIN_GOOGLE_CLIENT_SECRET", "")) + } else if authHandlerType == "github" { + oauthProvider = github.NewProvider(env("ORIGIN_GITHUB_CLIENT_ID", ""), env("ORIGIN_GITHUB_CLIENT_SECRET", "")) + } + + state := external.DefaultState() + success := handlers.AuthenticationSuccessHandlers{success, state.(handlers.AuthenticationSuccessHandler)} + oauthHandler, err := external.NewHandler(oauthProvider, state, c.MasterAddr+callbackPath, success, error) + if err != nil { + glog.Fatalf("unexpected error: %v", err) + } + + mux.Handle(callbackPath, oauthHandler) + authHandler = oauthHandler + case "password": + authHandler = &redirectAuthHandler{RedirectURL: OpenShiftLoginPrefix, ThenParam: "then"} + + success := handlers.AuthenticationSuccessHandlers{success, redirectSuccessHandler{}} + + login := login.NewLogin(emptyCsrf{}, &callbackPasswordAuthenticator{emptyPasswordAuth{}, success}, login.DefaultLoginFormRenderer) + login.Install(mux, OpenShiftLoginPrefix) + case "empty": + authHandler = emptyAuth{} + default: + glog.Fatalf("No AuthenticationHandler found that matches %v. The oauth server cannot start!", authHandlerType) + } + + return authHandler +} + +func (c *AuthConfig) getAuthenticationRequestHandler() authenticator.Request { + // TODO presumeably we'll want either a list of what we've got or a way to describe a registry of these + // hard-coded strings as a stand-in until it gets sorted out + var authRequestHandler authenticator.Request + authRequestHandlerType := env("ORIGIN_AUTH_REQUEST_HANDLER", "session") + switch authRequestHandlerType { + case "bearer": + tokenAuthenticator, err := GetTokenAuthenticator(c.EtcdHelper) + if err != nil { + glog.Fatalf("Error creating TokenAuthenticator: %v. The oauth server cannot start!", err) + } + authRequestHandler = bearertoken.New(tokenAuthenticator) + case "requestheader": + authRequestHandler = requestheader.NewAuthenticator(requestheader.NewDefaultConfig()) + case "session": + sessionStore := session.NewStore(c.SessionSecrets...) + authRequestHandler = session.NewSessionAuthenticator(sessionStore, "ssn") + default: + glog.Fatalf("No AuthenticationRequestHandler found that matches %v. The oauth server cannot start!", authRequestHandlerType) + } + + return authRequestHandler +} + +func GetTokenAuthenticator(etcdHelper tools.EtcdHelper) (authenticator.Token, error) { + tokenAuthenticatorType := env("ORIGIN_AUTH_TOKEN_AUTHENTICATOR", "etcd") + switch tokenAuthenticatorType { + case "etcd": + oauthRegistry := oauthetcd.New(etcdHelper) + return authnregistry.NewTokenAuthenticator(oauthRegistry), nil + case "file": + return authfile.NewTokenAuthenticator(env("ORIGIN_AUTH_FILE_TOKEN_AUTHENTICATOR_PATH", "authorizedTokens.csv")) + default: + return nil, errors.New(fmt.Sprintf("No TokenAuthenticator found that matches %v. The oauth server cannot start!", tokenAuthenticatorType)) + } +} + type emptyAuth struct{} func (emptyAuth) AuthenticationNeeded(w http.ResponseWriter, req *http.Request) { @@ -76,6 +179,9 @@ func (emptyAuth) AuthenticationNeeded(w http.ResponseWriter, req *http.Request) func (emptyAuth) AuthenticationError(err error, w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "
AuthenticationError - %s", err) } +func (emptyAuth) String() string { + return "emptyAuth" +} // Captures the original request url as a "then" param in a redirect to a login flow type redirectAuthHandler struct { @@ -103,10 +209,14 @@ func (auth *redirectAuthHandler) AuthenticationError(err error, w http.ResponseW fmt.Fprintf(w, "AuthenticationError - %s", err) } +func (auth *redirectAuthHandler) String() string { + return fmt.Sprintf("redirectAuth{url:%s, then:%s}", auth.RedirectURL, auth.ThenParam) +} + type emptyGrant struct{} -func (emptyGrant) GrantNeeded(grant *api.Grant, w http.ResponseWriter, req *http.Request) { - fmt.Fprintf(w, "GrantNeeded - not implemented%#v", grant) +func (emptyGrant) GrantNeeded(client api.Client, user api.UserInfo, grant *api.Grant, w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "GrantNeeded - not implemented
%#v\n%#v\n%#v", client, user, grant) } func (emptyGrant) GrantError(err error, w http.ResponseWriter, req *http.Request) { @@ -138,29 +248,34 @@ func (emptyPasswordAuth) AuthenticatePassword(user, password string) (api.UserIn } // -// Saves the username of any successful password authentication in the session +// Combines password auth, successful login callback, and "then" param redirection // -type sessionPasswordAuthenticator struct { - passwordAuthenticator authenticator.Password - sessionAuthenticator *session.SessionAuthenticator +type callbackPasswordAuthenticator struct { + authenticator.Password + handlers.AuthenticationSuccessHandler } -// for login.PasswordAuthenticator -func (auth *sessionPasswordAuthenticator) AuthenticatePassword(user, password string) (api.UserInfo, bool, error) { - return auth.passwordAuthenticator.AuthenticatePassword(user, password) -} +// Redirects to the then param on successful authentication +type redirectSuccessHandler struct{} -// for login.PasswordAuthenticator -func (auth *sessionPasswordAuthenticator) AuthenticationSucceeded(user api.UserInfo, then string, w http.ResponseWriter, req *http.Request) { - err := auth.sessionAuthenticator.AuthenticationSucceeded(user, w, req) - if err != nil { - fmt.Fprintf(w, "Could not save session, err=%#v", err) - return +func (redirectSuccessHandler) AuthenticationSucceeded(user api.UserInfo, then string, w http.ResponseWriter, req *http.Request) error { + if len(then) == 0 { + return fmt.Errorf("Auth succeeded, but no redirect existed - user=%#v", user) } - if len(then) != 0 { - http.Redirect(w, req, then, http.StatusFound) - } else { - fmt.Fprintf(w, "PasswordAuthenticationSucceeded - user=%#v", user) - } + http.Redirect(w, req, then, http.StatusFound) + return nil +} + +type emptySuccess struct{} + +func (emptySuccess) AuthenticationSucceeded(user api.UserInfo, state string, w http.ResponseWriter, req *http.Request) error { + glog.V(4).Infof("AuthenticationSucceeded: %v (state=%s)", user, state) + return nil +} + +type emptyError struct{} + +func (emptyError) AuthenticationError(err error, w http.ResponseWriter, req *http.Request) { + glog.V(4).Infof("AuthenticationError: %v", err) } diff --git a/pkg/cmd/server/origin/master.go b/pkg/cmd/server/origin/master.go index f8cbe836761f..36a5732bb6b3 100644 --- a/pkg/cmd/server/origin/master.go +++ b/pkg/cmd/server/origin/master.go @@ -21,6 +21,9 @@ import ( "github.com/openshift/origin/pkg/api/latest" "github.com/openshift/origin/pkg/api/v1beta1" "github.com/openshift/origin/pkg/assets" + "github.com/openshift/origin/pkg/auth/authenticator/bearertoken" + authcontext "github.com/openshift/origin/pkg/auth/context" + authfilter "github.com/openshift/origin/pkg/auth/handlers" "github.com/openshift/origin/pkg/build" buildapi "github.com/openshift/origin/pkg/build/api" buildregistry "github.com/openshift/origin/pkg/build/registry/build" @@ -75,7 +78,8 @@ type MasterConfig struct { MasterAddr string AssetAddr string - CORSAllowedOrigins []*regexp.Regexp + CORSAllowedOrigins []*regexp.Regexp + RequireAuthentication bool EtcdHelper tools.EtcdHelper @@ -181,6 +185,9 @@ func (c *MasterConfig) RunAPI(installers ...APIInstaller) { apiserver.InstallSupport(osMux) handler := http.Handler(osMux) + if c.RequireAuthentication { + handler = c.wrapHandlerWithAuthentication(handler) + } if len(c.CORSAllowedOrigins) > 0 { handler = apiserver.CORS(handler, c.CORSAllowedOrigins, nil, nil, "true") } @@ -204,6 +211,28 @@ func (c *MasterConfig) RunAPI(installers ...APIInstaller) { }, 0) } +func (c *MasterConfig) wrapHandlerWithAuthentication(handler http.Handler) http.Handler { + // wrap with authenticated token check + requestsToUsers := authcontext.NewRequestContextMap() // this tracks requests back to users for authorization + tokenAuthenticator, err := GetTokenAuthenticator(c.EtcdHelper) + if err != nil { + glog.Fatalf("Error creating TokenAuthenticator: %v. The oauth server cannot start!", err) + } + return authfilter.NewRequestAuthenticator( + requestsToUsers, + bearertoken.New(tokenAuthenticator), + http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + // TODO: make this failure handler actually fail once internal components can get auth tokens to do their job + // w.WriteHeader(http.StatusUnauthorized) + // return + + // For now, just let us know and continue on your merry way + glog.V(2).Infof("Token authentication failed when accessing: %v", req.URL) + handler.ServeHTTP(w, req) + }), + handler) +} + // RunAssetServer starts the asset server for the OpenShift UI. func (c *MasterConfig) RunAssetServer() { // TODO prefix should be able to be overridden at the command line diff --git a/pkg/cmd/server/start.go b/pkg/cmd/server/start.go index 33a5c4c6ce1d..47e7cf9365f9 100644 --- a/pkg/cmd/server/start.go +++ b/pkg/cmd/server/start.go @@ -77,7 +77,8 @@ type config struct { NodeList flagtypes.StringList - CORSAllowedOrigins flagtypes.StringList + CORSAllowedOrigins flagtypes.StringList + RequireAuthentication bool } func NewCommandStartServer(name string) *cobra.Command { @@ -167,10 +168,11 @@ func NewCommandStartServer(name string) *cobra.Command { assetAddr := net.JoinHostPort(cfg.MasterAddr.Host, strconv.Itoa(cfg.BindAddr.Port+1)) osmaster := &origin.MasterConfig{ - BindAddr: cfg.BindAddr.URL.Host, - MasterAddr: cfg.MasterAddr.URL.String(), - AssetAddr: assetAddr, - EtcdHelper: etcdHelper, + BindAddr: cfg.BindAddr.URL.Host, + MasterAddr: cfg.MasterAddr.URL.String(), + AssetAddr: assetAddr, + EtcdHelper: etcdHelper, + RequireAuthentication: cfg.RequireAuthentication, } // pick an appropriate Kube client @@ -188,6 +190,7 @@ func NewCommandStartServer(name string) *cobra.Command { osmaster.EnsureCORSAllowedOrigins(cfg.CORSAllowedOrigins) auth := &origin.AuthConfig{ + MasterAddr: cfg.MasterAddr.URL.String(), SessionSecrets: []string{"secret"}, EtcdHelper: etcdHelper, } @@ -264,6 +267,7 @@ func NewCommandStartServer(name string) *cobra.Command { flag.Var(&cfg.NodeList, "nodes", "The hostnames of each node. This currently must be specified up front. Comma delimited list") flag.Var(&cfg.CORSAllowedOrigins, "cors-allowed-origins", "List of allowed origins for CORS, comma separated. An allowed origin can be a regular expression to support subdomain matching. If this list is empty CORS will not be enabled.") + flag.BoolVar(&cfg.RequireAuthentication, "require-authentication", false, "Require authentication token for API access.") cfg.Docker.InstallFlags(flag) diff --git a/pkg/oauth/server/osinserver/osinserver.go b/pkg/oauth/server/osinserver/osinserver.go index 344c2063e152..32627ee912e8 100644 --- a/pkg/oauth/server/osinserver/osinserver.go +++ b/pkg/oauth/server/osinserver/osinserver.go @@ -88,3 +88,7 @@ func (s *Server) handleInfo(w http.ResponseWriter, r *http.Request) { } osin.OutputJSON(resp, w, r) } + +// func (s *Server) String() string { +// return fmt.Sprintf("osinserver.Server{config:%#v, authorize:%v, access:%v}", s.config, s.authorize, s.access) +// }