Browse code

add cli challenge interaction, kick travis again

deads2k authored on 2014/11/26 01:23:32
Showing 28 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,24 @@
0
+package handlers
1
+
2
+import (
3
+	"net/http"
4
+
5
+	oauthhandlers "github.com/openshift/origin/pkg/auth/oauth/handlers"
6
+)
7
+
8
+type basicPasswordAuthHandler struct {
9
+	realm string
10
+}
11
+
12
+// NewBasicPasswordAuthHandler returns a ChallengeAuthHandler that responds with a basic auth challenge for the supplied realm
13
+func NewBasicAuthChallenger(realm string) oauthhandlers.AuthenticationChallenger {
14
+	return &basicPasswordAuthHandler{realm}
15
+}
16
+
17
+// AuthenticationChallengeNeeded returns a header that indicates a basic auth challenge for the supplied realm
18
+func (h *basicPasswordAuthHandler) AuthenticationChallenge(req *http.Request) (http.Header, error) {
19
+	headers := http.Header{}
20
+	headers.Add("WWW-Authenticate", "Basic realm=\""+h.realm+"\"")
21
+
22
+	return headers, nil
23
+}
0 24
new file mode 100644
... ...
@@ -0,0 +1,27 @@
0
+package handlers
1
+
2
+import (
3
+	"net/http"
4
+	"testing"
5
+)
6
+
7
+func TestAuthChallengeNeeded(t *testing.T) {
8
+	handler := NewBasicAuthChallenger("testing-realm")
9
+	req := &http.Request{}
10
+	header, err := handler.AuthenticationChallenge(req)
11
+
12
+	if err != nil {
13
+		t.Errorf("Unexepcted error: %v", err)
14
+	}
15
+
16
+	if value, ok := header["Www-Authenticate"]; ok {
17
+		expectedValue := "Basic realm=\"testing-realm\""
18
+		if value[0] != expectedValue {
19
+			t.Errorf("Expected %v, got %v", expectedValue, value)
20
+		}
21
+	} else {
22
+		t.Error("Did not get back header")
23
+
24
+	}
25
+
26
+}
... ...
@@ -9,13 +9,17 @@ import (
9 9
 	"github.com/openshift/origin/pkg/auth/authenticator"
10 10
 )
11 11
 
12
+// TODO remove this in favor of kubernetes types
13
+
12 14
 type unionAuthRequestHandler []authenticator.Request
13 15
 
16
+// NewUnionAuthentication returns a request authenticator that validates credentials using a chain of authenticator.Request objects
14 17
 func NewUnionAuthentication(authRequestHandlers []authenticator.Request) authenticator.Request {
15
-	ret := unionAuthRequestHandler(authRequestHandlers)
16
-	return &ret
18
+	return unionAuthRequestHandler(authRequestHandlers)
17 19
 }
18 20
 
21
+// AuthenticateRequest authenticates the request using a chain of authenticator.Request objects.  The first
22
+// success returns that identity.  Errors are only returned if no matches are found.
19 23
 func (authHandler unionAuthRequestHandler) AuthenticateRequest(req *http.Request) (authapi.UserInfo, bool, error) {
20 24
 	var errors kutil.ErrorList
21 25
 	for _, currAuthRequestHandler := range authHandler {
... ...
@@ -26,7 +26,7 @@ type Handler struct {
26 26
 	mapper       authapi.UserIdentityMapper
27 27
 }
28 28
 
29
-func NewHandler(provider Provider, state State, redirectURL string, success handlers.AuthenticationSuccessHandler, errorHandler handlers.AuthenticationErrorHandler, mapper authapi.UserIdentityMapper) (*Handler, error) {
29
+func NewExternalOAuthRedirector(provider Provider, state State, redirectURL string, success handlers.AuthenticationSuccessHandler, errorHandler handlers.AuthenticationErrorHandler, mapper authapi.UserIdentityMapper) (*Handler, error) {
30 30
 	clientConfig, err := provider.NewConfig()
31 31
 	if err != nil {
32 32
 		return nil, err
... ...
@@ -50,8 +50,8 @@ func NewHandler(provider Provider, state State, redirectURL string, success hand
50 50
 	}, nil
51 51
 }
52 52
 
53
-// Implements oauth.handlers.AuthenticationHandler
54
-func (h *Handler) AuthenticationNeeded(w http.ResponseWriter, req *http.Request) (bool, error) {
53
+// Implements oauth.handlers.RedirectAuthHandler
54
+func (h *Handler) AuthenticationRedirect(w http.ResponseWriter, req *http.Request) error {
55 55
 	glog.V(4).Infof("Authentication needed for %v", h)
56 56
 
57 57
 	authReq := h.client.NewAuthorizeRequest(osincli.CODE)
... ...
@@ -60,14 +60,14 @@ func (h *Handler) AuthenticationNeeded(w http.ResponseWriter, req *http.Request)
60 60
 	state, err := h.state.Generate(w, req)
61 61
 	if err != nil {
62 62
 		glog.V(4).Infof("Error generating state: %v", err)
63
-		return h.errorHandler.AuthenticationError(err, w, req)
63
+		return err
64 64
 	}
65 65
 
66 66
 	oauthURL := authReq.GetAuthorizeUrlWithParams(state)
67 67
 	glog.V(4).Infof("redirect to %v", oauthURL)
68 68
 
69 69
 	http.Redirect(w, req, oauthURL.String(), http.StatusFound)
70
-	return true, nil
70
+	return nil
71 71
 }
72 72
 
73 73
 // Handles the callback request in response to an external oauth flow
... ...
@@ -7,5 +7,5 @@ import (
7 7
 )
8 8
 
9 9
 func TestHandler(t *testing.T) {
10
-	_ = handlers.AuthenticationHandler(&Handler{})
10
+	_ = handlers.NewUnionAuthenticationHandler(nil, map[string]handlers.AuthenticationRedirector{"handler": &Handler{}}, nil)
11 11
 }
... ...
@@ -31,7 +31,7 @@ func (h *AuthorizeAuthenticator) HandleAuthorize(ar *osin.AuthorizeRequest, w ht
31 31
 		return h.errorHandler.AuthenticationError(err, w, ar.HttpRequest)
32 32
 	}
33 33
 	if !ok {
34
-		return h.handler.AuthenticationNeeded(w, ar.HttpRequest)
34
+		return h.handler.AuthenticationNeeded(ar.Client, w, ar.HttpRequest)
35 35
 	}
36 36
 	ar.UserData = info
37 37
 	ar.Authorized = true
38 38
new file mode 100644
... ...
@@ -0,0 +1,112 @@
0
+package handlers
1
+
2
+import (
3
+	"fmt"
4
+	"net/http"
5
+
6
+	kutil "github.com/GoogleCloudPlatform/kubernetes/pkg/util"
7
+
8
+	authapi "github.com/openshift/origin/pkg/auth/api"
9
+	oauthapi "github.com/openshift/origin/pkg/oauth/api"
10
+)
11
+
12
+// unionAuthenticationHandler is an oauth.AuthenticationHandler that muxes multiple challange handlers and redirect handlers
13
+type unionAuthenticationHandler struct {
14
+	challengers  map[string]AuthenticationChallenger
15
+	redirectors  map[string]AuthenticationRedirector
16
+	errorHandler AuthenticationErrorHandler
17
+}
18
+
19
+// NewUnionAuthenticationHandler returns an oauth.AuthenticationHandler that muxes multiple challange handlers and redirect handlers
20
+func NewUnionAuthenticationHandler(passedChallengers map[string]AuthenticationChallenger, passedRedirectors map[string]AuthenticationRedirector, errorHandler AuthenticationErrorHandler) AuthenticationHandler {
21
+	challengers := passedChallengers
22
+	if challengers == nil {
23
+		challengers = make(map[string]AuthenticationChallenger, 1)
24
+	}
25
+
26
+	redirectors := passedRedirectors
27
+	if redirectors == nil {
28
+		redirectors = make(map[string]AuthenticationRedirector, 1)
29
+	}
30
+
31
+	return &unionAuthenticationHandler{challengers, redirectors, errorHandler}
32
+}
33
+
34
+// AuthenticationNeeded looks at the oauth Client to determine whether it wants try to authenticate with challenges or using a redirect path
35
+// If the client wants a challenge path, it muxes together all the different challenges from the challenge handlers
36
+// If (the client wants a redirect path) and ((there is one redirect handler) or (a redirect handler was requested via the "useRedirectHandler" parameter),
37
+// then the redirect handler is called.  Otherwise, you get an error (currently) or a redirect to a page letting you choose how you'd like to authenticate.
38
+// It returns whether the response was written and/or an error
39
+func (authHandler *unionAuthenticationHandler) AuthenticationNeeded(apiClient authapi.Client, w http.ResponseWriter, req *http.Request) (bool, error) {
40
+	client, ok := apiClient.GetUserData().(*oauthapi.Client)
41
+	if !ok {
42
+		return false, fmt.Errorf("apiClient data was not an oauthapi.Client")
43
+	}
44
+
45
+	if client.RespondWithChallenges {
46
+		var errors kutil.ErrorList
47
+		headers := http.Header(make(map[string][]string))
48
+		for _, challengingHandler := range authHandler.challengers {
49
+			currHeaders, err := challengingHandler.AuthenticationChallenge(req)
50
+			if err != nil {
51
+				errors = append(errors, err)
52
+				continue
53
+			}
54
+
55
+			// merge header values
56
+			mergeHeaders(headers, currHeaders)
57
+		}
58
+
59
+		if len(headers) > 0 {
60
+			mergeHeaders(w.Header(), headers)
61
+			w.WriteHeader(http.StatusUnauthorized)
62
+			return true, nil
63
+
64
+		} else {
65
+			return false, errors.ToError()
66
+		}
67
+
68
+	} else {
69
+		redirectHandlerName := req.URL.Query().Get("useRedirectHandler")
70
+
71
+		if len(redirectHandlerName) > 0 {
72
+			redirectHandler := authHandler.redirectors[redirectHandlerName]
73
+			if redirectHandler == nil {
74
+				return false, fmt.Errorf("Unable to locate redirect handler: %v", redirectHandlerName)
75
+			}
76
+
77
+			err := redirectHandler.AuthenticationRedirect(w, req)
78
+			if err != nil {
79
+				return authHandler.errorHandler.AuthenticationError(err, w, req)
80
+			}
81
+			return true, nil
82
+
83
+		} else {
84
+			if (len(authHandler.redirectors)) == 1 {
85
+				// there has to be a better way
86
+				for _, redirectHandler := range authHandler.redirectors {
87
+					err := redirectHandler.AuthenticationRedirect(w, req)
88
+					if err != nil {
89
+						return authHandler.errorHandler.AuthenticationError(err, w, req)
90
+					}
91
+					return true, nil
92
+				}
93
+			} else if len(authHandler.redirectors) > 1 {
94
+				// TODO this clearly doesn't work right.  There should probably be a redirect to an interstitial page.
95
+				// however, this is just as good as we have now.
96
+				return false, fmt.Errorf("Too many potential redirect handlers: %v", authHandler.redirectors)
97
+			}
98
+
99
+		}
100
+	}
101
+
102
+	return false, nil
103
+}
104
+
105
+func mergeHeaders(dest http.Header, toAdd http.Header) {
106
+	for key, values := range toAdd {
107
+		for _, value := range values {
108
+			dest.Add(key, value)
109
+		}
110
+	}
111
+}
0 112
new file mode 100644
... ...
@@ -0,0 +1,181 @@
0
+package handlers
1
+
2
+import (
3
+	"errors"
4
+	"net/http"
5
+	"net/http/httptest"
6
+	"reflect"
7
+	"strings"
8
+	"testing"
9
+
10
+	oauthapi "github.com/openshift/origin/pkg/oauth/api"
11
+)
12
+
13
+type testClient struct {
14
+	client *oauthapi.Client
15
+}
16
+
17
+func (w *testClient) GetId() string {
18
+	return w.client.Name
19
+}
20
+func (w *testClient) GetSecret() string {
21
+	return w.client.Secret
22
+}
23
+func (w *testClient) GetRedirectUri() string {
24
+	if len(w.client.RedirectURIs) == 0 {
25
+		return ""
26
+	}
27
+	return strings.Join(w.client.RedirectURIs, ",")
28
+}
29
+func (w *testClient) GetUserData() interface{} {
30
+	return w.client
31
+}
32
+
33
+type mockChallenger struct {
34
+	headerName  string
35
+	headerValue string
36
+	err         error
37
+}
38
+
39
+func (h *mockChallenger) AuthenticationChallenge(req *http.Request) (http.Header, error) {
40
+	headers := http.Header{}
41
+	if len(h.headerName) > 0 {
42
+		headers.Add(h.headerName, h.headerValue)
43
+	}
44
+
45
+	return headers, h.err
46
+}
47
+
48
+func TestNoHandlersRedirect(t *testing.T) {
49
+	authHandler := NewUnionAuthenticationHandler(nil, nil, nil)
50
+	client := &testClient{&oauthapi.Client{}}
51
+	req, _ := http.NewRequest("GET", "http://example.org", nil)
52
+	responseRecorder := httptest.NewRecorder()
53
+	handled, err := authHandler.AuthenticationNeeded(client, responseRecorder, req)
54
+
55
+	if err != nil {
56
+		t.Errorf("Unexepcted error: %v", err)
57
+	}
58
+
59
+	if handled {
60
+		t.Error("Unexpectedly handled.")
61
+	}
62
+}
63
+
64
+func TestNoHandlersChallenge(t *testing.T) {
65
+	authHandler := NewUnionAuthenticationHandler(nil, nil, nil)
66
+	client := &testClient{&oauthapi.Client{RespondWithChallenges: true}}
67
+	req, _ := http.NewRequest("GET", "http://example.org", nil)
68
+	responseRecorder := httptest.NewRecorder()
69
+	handled, err := authHandler.AuthenticationNeeded(client, responseRecorder, req)
70
+
71
+	if err != nil {
72
+		t.Errorf("Unexepcted error: %v", err)
73
+	}
74
+
75
+	if handled {
76
+		t.Error("Unexpectedly handled.")
77
+	}
78
+}
79
+
80
+func TestWithBadClient(t *testing.T) {
81
+	authHandler := NewUnionAuthenticationHandler(nil, nil, nil)
82
+	client := &badTestClient{&oauthapi.Client{}}
83
+	req, _ := http.NewRequest("GET", "http://example.org", nil)
84
+	responseRecorder := httptest.NewRecorder()
85
+	handled, err := authHandler.AuthenticationNeeded(client, responseRecorder, req)
86
+
87
+	expectedError := "apiClient data was not an oauthapi.Client"
88
+	if err == nil {
89
+		t.Errorf("Expected error: %v", expectedError)
90
+	}
91
+	if err.Error() != expectedError {
92
+		t.Errorf("Expected %v, got %v", expectedError, err)
93
+	}
94
+
95
+	if handled {
96
+		t.Error("Unexpectedly handled.")
97
+	}
98
+}
99
+
100
+func TestWithOnlyChallengeErrors(t *testing.T) {
101
+	expectedError1 := "alfa"
102
+	expectedError2 := "bravo"
103
+	failingChallengeHandler1 := &mockChallenger{err: errors.New(expectedError1)}
104
+	failingChallengeHandler2 := &mockChallenger{err: errors.New(expectedError2)}
105
+	authHandler := NewUnionAuthenticationHandler(
106
+		map[string]AuthenticationChallenger{"first": failingChallengeHandler1, "second": failingChallengeHandler2},
107
+		nil, nil)
108
+	client := &testClient{&oauthapi.Client{RespondWithChallenges: true}}
109
+	req, _ := http.NewRequest("GET", "http://example.org", nil)
110
+	responseRecorder := httptest.NewRecorder()
111
+	handled, err := authHandler.AuthenticationNeeded(client, responseRecorder, req)
112
+
113
+	if err == nil {
114
+		t.Errorf("Expected error: %v and %v", expectedError1, expectedError2)
115
+	}
116
+	if !strings.Contains(err.Error(), expectedError1) {
117
+		t.Errorf("Expected %v, got %v", expectedError1, err)
118
+	}
119
+	if !strings.Contains(err.Error(), expectedError2) {
120
+		t.Errorf("Expected %v, got %v", expectedError2, err)
121
+	}
122
+
123
+	if handled {
124
+		t.Error("Unexpectedly handled.")
125
+	}
126
+}
127
+
128
+func TestWithChallengeErrorsAndMergedSuccess(t *testing.T) {
129
+	expectedError := "failure"
130
+	failingChallengeHandler := &mockChallenger{err: errors.New(expectedError)}
131
+	workingChallengeHandler1 := &mockChallenger{headerName: "Charlie", headerValue: "delta"}
132
+	workingChallengeHandler2 := &mockChallenger{headerName: "Echo", headerValue: "foxtrot"}
133
+	workingChallengeHandler3 := &mockChallenger{headerName: "Charlie", headerValue: "golf"}
134
+
135
+	// order of the array is not guaranteed
136
+	expectedHeader1 := map[string][]string{"Charlie": {"delta", "golf"}, "Echo": {"foxtrot"}}
137
+	expectedHeader2 := map[string][]string{"Charlie": {"golf", "delta"}, "Echo": {"foxtrot"}}
138
+
139
+	authHandler := NewUnionAuthenticationHandler(
140
+		map[string]AuthenticationChallenger{
141
+			"first":  failingChallengeHandler,
142
+			"second": workingChallengeHandler1,
143
+			"third":  workingChallengeHandler2,
144
+			"fourth": workingChallengeHandler3},
145
+		nil, nil)
146
+	client := &testClient{&oauthapi.Client{RespondWithChallenges: true}}
147
+	req, _ := http.NewRequest("GET", "http://example.org", nil)
148
+	responseRecorder := httptest.NewRecorder()
149
+	handled, err := authHandler.AuthenticationNeeded(client, responseRecorder, req)
150
+
151
+	if err != nil {
152
+		t.Errorf("Unexepcted error: %v", err)
153
+	}
154
+	if !handled {
155
+		t.Error("Expected handling.")
156
+	}
157
+	if !(reflect.DeepEqual(map[string][]string(responseRecorder.HeaderMap), expectedHeader1) || reflect.DeepEqual(map[string][]string(responseRecorder.HeaderMap), expectedHeader2)) {
158
+		t.Errorf("Expected %#v or %#v, got %#v.", expectedHeader1, expectedHeader2, responseRecorder.HeaderMap)
159
+	}
160
+}
161
+
162
+type badTestClient struct {
163
+	client *oauthapi.Client
164
+}
165
+
166
+func (w *badTestClient) GetId() string {
167
+	return w.client.Name
168
+}
169
+func (w *badTestClient) GetSecret() string {
170
+	return w.client.Secret
171
+}
172
+func (w *badTestClient) GetRedirectUri() string {
173
+	if len(w.client.RedirectURIs) == 0 {
174
+		return ""
175
+	}
176
+	return strings.Join(w.client.RedirectURIs, ",")
177
+}
178
+func (w *badTestClient) GetUserData() interface{} {
179
+	return "w.client"
180
+}
... ...
@@ -5,18 +5,18 @@ import (
5 5
 
6 6
 	"github.com/golang/glog"
7 7
 
8
-	"github.com/openshift/origin/pkg/auth/api"
8
+	authapi "github.com/openshift/origin/pkg/auth/api"
9 9
 )
10 10
 
11 11
 type EmptyAuth struct{}
12 12
 
13
-func (EmptyAuth) AuthenticationNeeded(w http.ResponseWriter, req *http.Request) (bool, error) {
13
+func (EmptyAuth) AuthenticationNeeded(client authapi.Client, w http.ResponseWriter, req *http.Request) (bool, error) {
14 14
 	return false, nil
15 15
 }
16 16
 
17 17
 type EmptySuccess struct{}
18 18
 
19
-func (EmptySuccess) AuthenticationSucceeded(user api.UserInfo, state string, w http.ResponseWriter, req *http.Request) (bool, error) {
19
+func (EmptySuccess) AuthenticationSucceeded(user authapi.UserInfo, state string, w http.ResponseWriter, req *http.Request) (bool, error) {
20 20
 	glog.V(4).Infof("AuthenticationSucceeded: %v (state=%s)", user, state)
21 21
 	return false, nil
22 22
 }
... ...
@@ -9,7 +9,19 @@ import (
9 9
 // AuthenticationHandler reacts to unauthenticated requests
10 10
 type AuthenticationHandler interface {
11 11
 	// AuthenticationNeeded reacts to unauthenticated requests, and returns true if the response was written,
12
-	AuthenticationNeeded(w http.ResponseWriter, req *http.Request) (handled bool, err error)
12
+	AuthenticationNeeded(client api.Client, w http.ResponseWriter, req *http.Request) (handled bool, err error)
13
+}
14
+
15
+// AuthenticationChallenger reacts to unauthenticated requests with challenges
16
+type AuthenticationChallenger interface {
17
+	// AuthenticationChallenge take a request and return whatever challenge headers are appropriate.  If none are appropriate, it should return an empty map, not nil.
18
+	AuthenticationChallenge(req *http.Request) (header http.Header, err error)
19
+}
20
+
21
+// AuthenticationRedirector reacts to unauthenticated requests with redirects
22
+type AuthenticationRedirector interface {
23
+	// AuthenticationRedirect is expected to write a redirect to the ResponseWriter or to return an error.
24
+	AuthenticationRedirect(w http.ResponseWriter, req *http.Request) (err error)
13 25
 }
14 26
 
15 27
 // AuthenticationErrorHandler reacts to authentication errors
... ...
@@ -38,7 +38,7 @@ type testHandlers struct {
38 38
 	HandledErr error
39 39
 }
40 40
 
41
-func (h *testHandlers) AuthenticationNeeded(w http.ResponseWriter, req *http.Request) (bool, error) {
41
+func (h *testHandlers) AuthenticationNeeded(client api.Client, w http.ResponseWriter, req *http.Request) (bool, error) {
42 42
 	h.AuthNeed = true
43 43
 	return h.AuthNeedHandled, h.AuthNeedErr
44 44
 }
... ...
@@ -8,7 +8,6 @@ import (
8 8
 
9 9
 	"github.com/RangelReale/osincli"
10 10
 
11
-	"github.com/openshift/origin/pkg/auth/oauth/handlers"
12 11
 	"github.com/openshift/origin/pkg/auth/server/login"
13 12
 )
14 13
 
... ...
@@ -18,7 +17,6 @@ const (
18 18
 )
19 19
 
20 20
 type endpointDetails struct {
21
-	authHandler handlers.AuthenticationHandler
22 21
 	// Jordan has done the osincli review and declares the osinclit.Client reusable and thread-safe
23 22
 	originOAuthClient *osincli.Client
24 23
 }
... ...
@@ -27,8 +25,8 @@ type Endpoints interface {
27 27
 	Install(mux login.Mux, paths ...string)
28 28
 }
29 29
 
30
-func NewEndpoints(authHandler handlers.AuthenticationHandler, originOAuthClient *osincli.Client) Endpoints {
31
-	return &endpointDetails{authHandler, originOAuthClient}
30
+func NewEndpoints(originOAuthClient *osincli.Client) Endpoints {
31
+	return &endpointDetails{originOAuthClient}
32 32
 }
33 33
 
34 34
 // Install registers the request token endpoints into a mux. It is expected that the
35 35
new file mode 100644
... ...
@@ -0,0 +1,29 @@
0
+package tokens
1
+
2
+import (
3
+	"fmt"
4
+	"os"
5
+
6
+	"github.com/spf13/cobra"
7
+
8
+	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
9
+	"github.com/openshift/origin/pkg/cmd/util/tokencmd"
10
+)
11
+
12
+func NewCmdRequestToken(clientCfg *clientcmd.Config) *cobra.Command {
13
+	cmd := &cobra.Command{
14
+		Use:   "request-token",
15
+		Short: "request an access token",
16
+		Long:  `request an access token`,
17
+		Run: func(cmd *cobra.Command, args []string) {
18
+			accessToken, err := tokencmd.RequestToken(clientCfg, os.Stdin)
19
+			if err != nil {
20
+				fmt.Printf("%v\n", err)
21
+				return
22
+			}
23
+
24
+			fmt.Printf("%v\n", string(accessToken))
25
+		},
26
+	}
27
+	return cmd
28
+}
0 29
new file mode 100644
... ...
@@ -0,0 +1,54 @@
0
+package tokens
1
+
2
+import (
3
+	"github.com/golang/glog"
4
+	"github.com/spf13/cobra"
5
+
6
+	"github.com/openshift/origin/pkg/auth/server/tokenrequest"
7
+	"github.com/openshift/origin/pkg/cmd/server/origin"
8
+	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
9
+)
10
+
11
+const (
12
+	TOKEN_FILE_PARAM = "token-file"
13
+)
14
+
15
+func NewCommandTokens(name string) *cobra.Command {
16
+	// Parent command to which all subcommands are added.
17
+	cmds := &cobra.Command{
18
+		Use:   name,
19
+		Short: "manage authentication tokens",
20
+		Long:  `manage authentication tokens`,
21
+		Run:   runHelp,
22
+	}
23
+
24
+	// copied out of kubernetes kubectl so that I'll be ready when that and osc finally merge in
25
+	// Globally persistent flags across all subcommands.
26
+	// TODO Change flag names to consts to allow safer lookup from subcommands.
27
+	// TODO Add a verbose flag that turns on glog logging. Probably need a way
28
+	// to do that automatically for every subcommand.
29
+	clientCfg := clientcmd.NewConfig()
30
+	clientCfg.Bind(cmds.PersistentFlags())
31
+
32
+	cmds.AddCommand(NewCmdValidateToken(clientCfg))
33
+	cmds.AddCommand(NewCmdRequestToken(clientCfg))
34
+	cmds.AddCommand(NewCmdWhoAmI(clientCfg))
35
+
36
+	return cmds
37
+}
38
+
39
+func runHelp(cmd *cobra.Command, args []string) {
40
+	cmd.Help()
41
+}
42
+
43
+func getFlagString(cmd *cobra.Command, flag string) string {
44
+	f := cmd.Flags().Lookup(flag)
45
+	if f == nil {
46
+		glog.Fatalf("Flag accessed but not defined for command %s: %s", cmd.Name(), flag)
47
+	}
48
+	return f.Value.String()
49
+}
50
+
51
+func getRequestTokenUrl(clientCfg *clientcmd.Config) string {
52
+	return clientCfg.KubeConfig().Host + origin.OpenShiftLoginPrefix + tokenrequest.RequestTokenEndpoint
53
+}
0 54
new file mode 100644
... ...
@@ -0,0 +1,92 @@
0
+package tokens
1
+
2
+import (
3
+	"encoding/json"
4
+	"fmt"
5
+
6
+	"github.com/golang/glog"
7
+	"github.com/spf13/cobra"
8
+
9
+	osclient "github.com/openshift/origin/pkg/client"
10
+	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
11
+	"github.com/openshift/origin/pkg/oauth/osintypes"
12
+)
13
+
14
+func NewCmdValidateToken(clientCfg *clientcmd.Config) *cobra.Command {
15
+	cmd := &cobra.Command{
16
+		Use:   "validate-token",
17
+		Short: "validate an access token",
18
+		Long:  `validate an access token`,
19
+		Run: func(cmd *cobra.Command, args []string) {
20
+			tokenValue := getFlagString(cmd, "token")
21
+			validateToken(tokenValue, clientCfg)
22
+		},
23
+	}
24
+	cmd.Flags().String("token", "", "Token value")
25
+	return cmd
26
+}
27
+
28
+func validateToken(token string, clientCfg *clientcmd.Config) {
29
+	if len(token) == 0 {
30
+		fmt.Println("You must provide a token to validate")
31
+		return
32
+	}
33
+	fmt.Printf("Using token: %v\n", token)
34
+
35
+	// This will be pulled out of the auth config file after https://github.com/GoogleCloudPlatform/kubernetes/pull/2437
36
+	osCfg := clientCfg.OpenShiftConfig()
37
+	osCfg.BearerToken = token
38
+
39
+	osClient, err := osclient.New(osCfg)
40
+	if err != nil {
41
+		fmt.Printf("Error building osClient: %v\n", err)
42
+		return
43
+	}
44
+
45
+	jsonResponse, _, err := getTokenInfo(token, osClient)
46
+	if err != nil {
47
+		fmt.Printf("%v\n", err)
48
+		fmt.Println("Try visiting " + getRequestTokenUrl(clientCfg) + " for a new token.")
49
+		return
50
+	}
51
+	fmt.Printf("%v\n", string(jsonResponse))
52
+
53
+	whoami, err := osClient.Users().Get("~")
54
+	if err != nil {
55
+		fmt.Printf("Error making whoami request: %v\n", err)
56
+		return
57
+	}
58
+	whoamiJson, err := json.Marshal(whoami)
59
+	if err != nil {
60
+		fmt.Printf("Error interpretting whoami response: %v\n", err)
61
+		return
62
+	}
63
+	fmt.Printf("%v\n", string(whoamiJson))
64
+}
65
+
66
+func getTokenInfo(token string, osClient *osclient.Client) (string, *osintypes.InfoResponseData, error) {
67
+	osResult := osClient.Get().AbsPath("oauth").Path("info").Param("code", token).Do()
68
+	if osResult.Error() != nil {
69
+		return "", nil, fmt.Errorf("Error making info request: %v", osResult.Error())
70
+	}
71
+	body, err := osResult.Raw()
72
+	if err != nil {
73
+		return "", nil, fmt.Errorf("Error reading info response: %v\n", err)
74
+	}
75
+	glog.V(1).Infof("Raw JSON: %v\n", string(body))
76
+
77
+	var accessData osintypes.InfoResponseData
78
+	err = json.Unmarshal(body, &accessData)
79
+	if err != nil {
80
+		return "", nil, fmt.Errorf("Error while unmarshalling info response: %v %v", err, string(body))
81
+	}
82
+	if accessData.Error == "invalid_request" {
83
+		return "", nil, fmt.Errorf("\"%v\" is not a valid token.\n", token)
84
+	}
85
+	if len(accessData.ErrorDescription) != 0 {
86
+		return "", nil, fmt.Errorf("%v\n", accessData.ErrorDescription)
87
+	}
88
+
89
+	return string(body), &accessData, nil
90
+
91
+}
0 92
new file mode 100644
... ...
@@ -0,0 +1,66 @@
0
+package tokens
1
+
2
+import (
3
+	"fmt"
4
+	"os"
5
+
6
+	"github.com/spf13/cobra"
7
+
8
+	osclient "github.com/openshift/origin/pkg/client"
9
+	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
10
+	"github.com/openshift/origin/pkg/cmd/util/tokencmd"
11
+)
12
+
13
+func NewCmdWhoAmI(clientCfg *clientcmd.Config) *cobra.Command {
14
+	cmd := &cobra.Command{
15
+		Use:   "whoami",
16
+		Short: "checks the identity associated with an access token",
17
+		Long:  `checks the identity associated with an access token`,
18
+		Run: func(cmd *cobra.Command, args []string) {
19
+			token := ""
20
+			if cmd.Flags().Lookup("token") != nil {
21
+				token = getFlagString(cmd, "token")
22
+			}
23
+			whoami(token, clientCfg)
24
+
25
+		},
26
+	}
27
+	cmd.Flags().String("token", "", "Token value")
28
+	return cmd
29
+}
30
+
31
+func whoami(token string, clientCfg *clientcmd.Config) {
32
+	osCfg := clientCfg.OpenShiftConfig()
33
+	// This will be pulled out of the auth config file after https://github.com/GoogleCloudPlatform/kubernetes/pull/2437
34
+	if len(token) > 0 {
35
+		osCfg.BearerToken = token
36
+	}
37
+
38
+	osClient, err := osclient.New(osCfg)
39
+	if err != nil {
40
+		fmt.Printf("Error building osClient: %v\n", err)
41
+		return
42
+	}
43
+
44
+	me, err := osClient.Users().Get("~")
45
+	if err != nil {
46
+		// let's pretend that we can determine that we got back a 401.  we need an updated kubernetes for this
47
+		accessToken, err := tokencmd.RequestToken(clientCfg, os.Stdin)
48
+		if err != nil {
49
+			fmt.Printf("%v\n", err)
50
+			return
51
+		}
52
+
53
+		osCfg := clientCfg.OpenShiftConfig()
54
+		osCfg.BearerToken = accessToken
55
+		osClient, _ = osclient.New(osCfg)
56
+
57
+		me, err = osClient.Users().Get("~")
58
+		if err != nil {
59
+			fmt.Printf("Error making request: %v\n", err)
60
+			return
61
+		}
62
+	}
63
+
64
+	fmt.Printf("%v\n", me)
65
+}
0 66
new file mode 100644
... ...
@@ -0,0 +1,21 @@
0
+package experimental
1
+
2
+import (
3
+	"github.com/spf13/cobra"
4
+
5
+	"github.com/openshift/origin/pkg/cmd/experimental/tokens"
6
+)
7
+
8
+func NewCommandExperimental(name string) *cobra.Command {
9
+	cmd := &cobra.Command{
10
+		Use: name,
11
+		Run: func(c *cobra.Command, args []string) {
12
+			// no usage
13
+			// c.Help()
14
+		},
15
+	}
16
+
17
+	cmd.AddCommand(tokens.NewCommandTokens("tokens"))
18
+
19
+	return cmd
20
+}
... ...
@@ -11,6 +11,7 @@ import (
11 11
 	"github.com/openshift/origin/pkg/cmd/flagtypes"
12 12
 	"github.com/openshift/origin/pkg/cmd/infra/builder"
13 13
 	"github.com/openshift/origin/pkg/cmd/infra/deployer"
14
+	"github.com/openshift/origin/pkg/cmd/infra/experimental"
14 15
 	"github.com/openshift/origin/pkg/cmd/infra/router"
15 16
 	"github.com/openshift/origin/pkg/cmd/server"
16 17
 	"github.com/openshift/origin/pkg/version"
... ...
@@ -48,6 +49,8 @@ func CommandFor(basename string) *cobra.Command {
48 48
 		return builder.NewCommandDockerBuilder(basename)
49 49
 	case "cli":
50 50
 		return cli.NewCommandCLI(basename)
51
+	case "openshift-experimental":
52
+		return experimental.NewCommandExperimental(basename)
51 53
 	default:
52 54
 		return NewCommandOpenShift()
53 55
 	}
... ...
@@ -79,6 +82,7 @@ func NewCommandOpenShift() *cobra.Command {
79 79
 		deployer.NewCommandDeployer("deploy"),
80 80
 		builder.NewCommandSTIBuilder("sti-build"),
81 81
 		builder.NewCommandDockerBuilder("docker-build"),
82
+		experimental.NewCommandExperimental("experimental"),
82 83
 	)
83 84
 	root.AddCommand(infra)
84 85
 
... ...
@@ -18,6 +18,7 @@ import (
18 18
 	"github.com/openshift/origin/pkg/auth/authenticator/basicauthpassword"
19 19
 	"github.com/openshift/origin/pkg/auth/authenticator/bearertoken"
20 20
 	authfile "github.com/openshift/origin/pkg/auth/authenticator/file"
21
+	authhandlers "github.com/openshift/origin/pkg/auth/authenticator/handlers"
21 22
 	"github.com/openshift/origin/pkg/auth/authenticator/requesthandlers"
22 23
 	"github.com/openshift/origin/pkg/auth/authenticator/requestheader"
23 24
 	"github.com/openshift/origin/pkg/auth/oauth/external"
... ...
@@ -36,6 +37,7 @@ import (
36 36
 	cmdutil "github.com/openshift/origin/pkg/cmd/util"
37 37
 	oauthapi "github.com/openshift/origin/pkg/oauth/api"
38 38
 	clientregistry "github.com/openshift/origin/pkg/oauth/registry/client"
39
+	oauthclient "github.com/openshift/origin/pkg/oauth/registry/client"
39 40
 	"github.com/openshift/origin/pkg/oauth/registry/clientauthorization"
40 41
 	oauthetcd "github.com/openshift/origin/pkg/oauth/registry/etcd"
41 42
 	"github.com/openshift/origin/pkg/oauth/server/osinserver"
... ...
@@ -59,6 +61,13 @@ var (
59 59
 		},
60 60
 		Secret: uuid.NewUUID().String(), // random secret so no one knows what it is ahead of time.  This still allows us to loop back for /requestToken
61 61
 	}
62
+	OSCliClientBase = oauthapi.Client{
63
+		ObjectMeta: kapi.ObjectMeta{
64
+			Name: "openshift-challenging-client",
65
+		},
66
+		Secret:                uuid.NewUUID().String(), // random secret so no one knows what it is ahead of time.  This still allows us to loop back for /requestToken
67
+		RespondWithChallenges: true,
68
+	}
62 69
 )
63 70
 
64 71
 type AuthConfig struct {
... ...
@@ -104,11 +113,11 @@ func (c *AuthConfig) InstallAPI(mux cmdutil.Mux) []string {
104 104
 	)
105 105
 	server.Install(mux, OpenShiftOAuthAPIPrefix)
106 106
 
107
-	c.createOrUpdateDefaultOAuthClients()
107
+	CreateOrUpdateDefaultOAuthClients(c.MasterAddr, oauthEtcd)
108 108
 	osOAuthClientConfig := c.NewOpenShiftOAuthClientConfig(&OSBrowserClientBase)
109 109
 	osOAuthClientConfig.RedirectUrl = c.MasterAddr + OpenShiftOAuthAPIPrefix + tokenrequest.DisplayTokenEndpoint
110 110
 	osOAuthClient, _ := osincli.NewClient(osOAuthClientConfig)
111
-	tokenRequestEndpoints := tokenrequest.NewEndpoints(authHandler, osOAuthClient)
111
+	tokenRequestEndpoints := tokenrequest.NewEndpoints(osOAuthClient)
112 112
 	tokenRequestEndpoints.Install(mux, OpenShiftOAuthAPIPrefix)
113 113
 
114 114
 	// glog.Infof("oauth server configured as: %#v", server)
... ...
@@ -137,25 +146,32 @@ func (c *AuthConfig) NewOpenShiftOAuthClientConfig(client *oauthapi.Client) *osi
137 137
 	return config
138 138
 }
139 139
 
140
-func (c *AuthConfig) createOrUpdateDefaultOAuthClients() {
140
+func CreateOrUpdateDefaultOAuthClients(masterAddr string, clientRegistry oauthclient.Registry) {
141 141
 	clientsToEnsure := []*oauthapi.Client{
142 142
 		{
143 143
 			ObjectMeta: kapi.ObjectMeta{
144 144
 				Name: OSBrowserClientBase.Name,
145 145
 			},
146
-			Secret:       OSBrowserClientBase.Secret,
147
-			RedirectURIs: []string{c.MasterAddr + OpenShiftOAuthAPIPrefix + tokenrequest.DisplayTokenEndpoint},
146
+			Secret:                OSBrowserClientBase.Secret,
147
+			RespondWithChallenges: OSBrowserClientBase.RespondWithChallenges,
148
+			RedirectURIs:          []string{masterAddr + OpenShiftOAuthAPIPrefix + tokenrequest.DisplayTokenEndpoint},
149
+		},
150
+		{
151
+			ObjectMeta: kapi.ObjectMeta{
152
+				Name: OSCliClientBase.Name,
153
+			},
154
+			Secret:                OSCliClientBase.Secret,
155
+			RespondWithChallenges: OSCliClientBase.RespondWithChallenges,
156
+			RedirectURIs:          []string{masterAddr + OpenShiftOAuthAPIPrefix + tokenrequest.DisplayTokenEndpoint},
148 157
 		},
149 158
 	}
150 159
 
151
-	oauthEtcd := oauthetcd.New(c.EtcdHelper)
152
-
153 160
 	for _, currClient := range clientsToEnsure {
154
-		if existing, err := oauthEtcd.GetClient(currClient.Name); err == nil || strings.Contains(err.Error(), " not found") {
161
+		if existing, err := clientRegistry.GetClient(currClient.Name); err == nil || strings.Contains(err.Error(), " not found") {
155 162
 			if existing != nil {
156
-				oauthEtcd.DeleteClient(currClient.Name)
163
+				clientRegistry.DeleteClient(currClient.Name)
157 164
 			}
158
-			if err = oauthEtcd.CreateClient(currClient); err != nil {
165
+			if err = clientRegistry.CreateClient(currClient); err != nil {
159 166
 				glog.Errorf("Error creating client: %v due to %v\n", currClient, err)
160 167
 			}
161 168
 		} else {
... ...
@@ -201,7 +217,7 @@ func (c *AuthConfig) getGrantHandler(mux cmdutil.Mux, auth authenticator.Request
201 201
 	return grantHandler
202 202
 }
203 203
 
204
-func (c *AuthConfig) getAuthenticationHandler(mux cmdutil.Mux, sessionStore session.Store, error handlers.AuthenticationErrorHandler) handlers.AuthenticationHandler {
204
+func (c *AuthConfig) getAuthenticationHandler(mux cmdutil.Mux, sessionStore session.Store, errorHandler handlers.AuthenticationErrorHandler) handlers.AuthenticationHandler {
205 205
 	successHandler := c.getAuthenticationSuccessHandler(sessionStore)
206 206
 
207 207
 	// TODO presumeably we'll want either a list of what we've got or a way to describe a registry of these
... ...
@@ -222,16 +238,20 @@ func (c *AuthConfig) getAuthenticationHandler(mux cmdutil.Mux, sessionStore sess
222 222
 		}
223 223
 
224 224
 		state := external.DefaultState()
225
-		oauthHandler, err := external.NewHandler(oauthProvider, state, c.MasterAddr+callbackPath, successHandler, error, identityMapper)
225
+		oauthHandler, err := external.NewExternalOAuthRedirector(oauthProvider, state, c.MasterAddr+callbackPath, successHandler, errorHandler, identityMapper)
226 226
 		if err != nil {
227 227
 			glog.Fatalf("unexpected error: %v", err)
228 228
 		}
229 229
 
230 230
 		mux.Handle(callbackPath, oauthHandler)
231
-		authHandler = oauthHandler
231
+		authHandler = handlers.NewUnionAuthenticationHandler(nil, map[string]handlers.AuthenticationRedirector{authHandlerType: oauthHandler}, errorHandler)
232 232
 	case "login":
233 233
 		passwordAuth := c.getPasswordAuthenticator()
234
-		authHandler = &redirectAuthHandler{RedirectURL: OpenShiftLoginPrefix, ThenParam: "then"}
234
+		authHandler = handlers.NewUnionAuthenticationHandler(
235
+			map[string]handlers.AuthenticationChallenger{"login": authhandlers.NewBasicAuthChallenger("openshift")},
236
+			map[string]handlers.AuthenticationRedirector{"login": &redirector{RedirectURL: OpenShiftLoginPrefix, ThenParam: "then"}},
237
+			errorHandler,
238
+		)
235 239
 		login := login.NewLogin(getCSRF(), &callbackPasswordAuthenticator{passwordAuth, successHandler}, login.DefaultLoginFormRenderer)
236 240
 		login.Install(mux, OpenShiftLoginPrefix)
237 241
 	case "empty":
... ...
@@ -347,16 +367,16 @@ func GetTokenAuthenticator(etcdHelper tools.EtcdHelper) (authenticator.Token, er
347 347
 }
348 348
 
349 349
 // Captures the original request url as a "then" param in a redirect to a login flow
350
-type redirectAuthHandler struct {
350
+type redirector struct {
351 351
 	RedirectURL string
352 352
 	ThenParam   string
353 353
 }
354 354
 
355
-// AuthenticationNeeded redirects HTTP request to authorization URL
356
-func (auth *redirectAuthHandler) AuthenticationNeeded(w http.ResponseWriter, req *http.Request) (bool, error) {
355
+// AuthenticationRedirectNeeded redirects HTTP request to authorization URL
356
+func (auth *redirector) AuthenticationRedirect(w http.ResponseWriter, req *http.Request) error {
357 357
 	redirectURL, err := url.Parse(auth.RedirectURL)
358 358
 	if err != nil {
359
-		return false, err
359
+		return err
360 360
 	}
361 361
 	if len(auth.ThenParam) != 0 {
362 362
 		redirectURL.RawQuery = url.Values{
... ...
@@ -364,7 +384,7 @@ func (auth *redirectAuthHandler) AuthenticationNeeded(w http.ResponseWriter, req
364 364
 		}.Encode()
365 365
 	}
366 366
 	http.Redirect(w, req, redirectURL.String(), http.StatusFound)
367
-	return true, nil
367
+	return nil
368 368
 }
369 369
 
370 370
 //
... ...
@@ -181,12 +181,12 @@ func (c *MasterConfig) RunAPI(installers ...APIInstaller) {
181 181
 	}
182 182
 	handlerContainer := kmaster.NewHandlerContainer(osMux)
183 183
 	apiserver.NewAPIGroupVersion(storage, v1beta1.Codec, OpenShiftAPIPrefixV1Beta1, latest.SelfLinker).InstallREST(handlerContainer, "/osapi", "v1beta1")
184
-	//apiserver.InstallSupport(osMux)
185 184
 
186 185
 	handler := http.Handler(osMux)
187 186
 	if c.RequireAuthentication {
188
-		handler = c.wrapHandlerWithAuthentication(handler)
187
+		handler = c.wireAuthenticationHandling(osMux, handler)
189 188
 	}
189
+
190 190
 	if len(c.CORSAllowedOrigins) > 0 {
191 191
 		handler = apiserver.CORS(handler, c.CORSAllowedOrigins, nil, nil, "true")
192 192
 	}
... ...
@@ -210,9 +210,37 @@ func (c *MasterConfig) RunAPI(installers ...APIInstaller) {
210 210
 	}, 0)
211 211
 }
212 212
 
213
-func (c *MasterConfig) wrapHandlerWithAuthentication(handler http.Handler) http.Handler {
213
+// wireAuthenticationHandling creates and binds all the objects that we only care about if authentication is turned on.  It's pulled out
214
+// just to make the RunAPI method easier to read.  These resources include the requestsToUsers map that allows callers to know which user
215
+// is requesting an operation, the handler wrapper that protects the passed handler behind a handler that requires valid authentication
216
+// information on the request, and an endpoint that only functions properly with an authenticated user.
217
+func (c *MasterConfig) wireAuthenticationHandling(osMux *http.ServeMux, handler http.Handler) http.Handler {
218
+	// this tracks requests back to users for authorization.  The same instance must be shared between writers and readers
219
+	requestsToUsers := authcontext.NewRequestContextMap()
220
+
221
+	// wrapHandlerWithAuthentication binds a handler that will correlate the users and requests
222
+	handler = c.wrapHandlerWithAuthentication(handler, requestsToUsers)
223
+
224
+	// this requires the requests and users to be present
225
+	thisUserEndpoint := OpenShiftAPIPrefixV1Beta1 + "/users/~"
226
+	userContextMap := userregistry.ContextFunc(func(req *http.Request) (userregistry.Info, bool) {
227
+		obj, found := requestsToUsers.Get(req)
228
+		if user, ok := obj.(userregistry.Info); found && ok {
229
+			return user, true
230
+		}
231
+		return nil, false
232
+	})
233
+	userregistry.InstallThisUser(osMux, thisUserEndpoint, userContextMap, handler)
234
+
235
+	return handler
236
+}
237
+
238
+// wrapHandlerWithAuthentication takes a handler and protects it behind a handler that tests to make sure that a user is authenticated.
239
+// if the request does have value auth information, then the request is allowed through the passed handler.  If the request does not have
240
+// valid auth information, then the request is passed to a failure handler.  Until we get authentication for system componenets, the
241
+// failure handler logs and passes through.
242
+func (c *MasterConfig) wrapHandlerWithAuthentication(handler http.Handler, requestsToUsers *authcontext.RequestContextMap) http.Handler {
214 243
 	// wrap with authenticated token check
215
-	requestsToUsers := authcontext.NewRequestContextMap() // this tracks requests back to users for authorization
216 244
 	tokenAuthenticator, err := GetTokenAuthenticator(c.EtcdHelper)
217 245
 	if err != nil {
218 246
 		glog.Fatalf("Error creating TokenAuthenticator: %v.  The oauth server cannot start!", err)
... ...
@@ -56,7 +56,7 @@ func (cfg *Config) bindEnv() {
56 56
 	}
57 57
 }
58 58
 
59
-func (cfg *Config) Clients() (kclient.Interface, osclient.Interface, error) {
59
+func (cfg *Config) KubeConfig() *kclient.Config {
60 60
 	cfg.bindEnv()
61 61
 
62 62
 	kaddr := cfg.KubernetesAddr
... ...
@@ -66,14 +66,28 @@ func (cfg *Config) Clients() (kclient.Interface, osclient.Interface, error) {
66 66
 
67 67
 	kConfig := cfg.CommonConfig
68 68
 	kConfig.Host = kaddr.URL.String()
69
-	kubeClient, err := kclient.New(&kConfig)
69
+
70
+	return &kConfig
71
+}
72
+
73
+func (cfg *Config) OpenShiftConfig() *kclient.Config {
74
+	cfg.bindEnv()
75
+
76
+	osConfig := cfg.CommonConfig
77
+	osConfig.Host = cfg.MasterAddr.String()
78
+
79
+	return &osConfig
80
+}
81
+
82
+func (cfg *Config) Clients() (kclient.Interface, osclient.Interface, error) {
83
+	cfg.bindEnv()
84
+
85
+	kubeClient, err := kclient.New(cfg.KubeConfig())
70 86
 	if err != nil {
71 87
 		return nil, nil, fmt.Errorf("Unable to configure Kubernetes client: %v", err)
72 88
 	}
73 89
 
74
-	osConfig := cfg.CommonConfig
75
-	osConfig.Host = cfg.MasterAddr.String()
76
-	osClient, err := osclient.New(&osConfig)
90
+	osClient, err := osclient.New(cfg.OpenShiftConfig())
77 91
 	if err != nil {
78 92
 		return nil, nil, fmt.Errorf("Unable to configure OpenShift client: %v", err)
79 93
 	}
80 94
new file mode 100644
... ...
@@ -0,0 +1,126 @@
0
+package tokencmd
1
+
2
+import (
3
+	"errors"
4
+	"fmt"
5
+	"io"
6
+	"net/http"
7
+	"regexp"
8
+	"strings"
9
+
10
+	kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
11
+
12
+	"github.com/openshift/origin/pkg/auth/server/tokenrequest"
13
+	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
14
+)
15
+
16
+const (
17
+	accessTokenRedirectPattern = `#access_token=([\w]+)&`
18
+)
19
+
20
+var (
21
+	accessTokenRedirectRegex = regexp.MustCompile(accessTokenRedirectPattern)
22
+)
23
+
24
+type tokenGetterInfo struct {
25
+	accessToken string
26
+}
27
+
28
+// RequestToken uses the cmd arguments to locate an openshift oauth server and attempts to authenticate
29
+// it returns the access token if it gets one.  An error if it does not
30
+func RequestToken(clientCfg *clientcmd.Config, reader io.Reader) (string, error) {
31
+	tokenGetter := &tokenGetterInfo{}
32
+
33
+	kubeCfg := clientCfg.KubeConfig()
34
+	kubeClient, err := kclient.New(kubeCfg)
35
+	if err != nil {
36
+		return "", err
37
+	}
38
+
39
+	// get the transport, so that we can use it to build our own client that wraps it
40
+	// our client understands certain challenges and can respond to them
41
+	clientTransport, err := kclient.TransportFor(kubeCfg)
42
+	if err != nil {
43
+		return "", err
44
+	}
45
+	httpClient := &http.Client{
46
+		Transport:     clientTransport,
47
+		CheckRedirect: tokenGetter.checkRedirect,
48
+	}
49
+	kubeClient.Client = &challengingClient{httpClient, reader}
50
+
51
+	_ = kubeClient.Get().AbsPath("oauth").Path("authorize").Param("response_type", "token").Param("client_id", "openshift-challenging-client").Do()
52
+
53
+	if len(tokenGetter.accessToken) == 0 {
54
+		requestTokenUrl := kubeCfg.Host + "/oauth" /* clean up after auth.go dies */ + tokenrequest.RequestTokenEndpoint
55
+		return "", errors.New("Unable to get token.  Try visiting " + requestTokenUrl + " for a new token.")
56
+	}
57
+
58
+	return tokenGetter.accessToken, nil
59
+}
60
+
61
+// checkRedirect watches the redirects to see if any contain the access_token anchor.  It then stores the value of the access token for later retrieval
62
+func (tokenGetter *tokenGetterInfo) checkRedirect(req *http.Request, via []*http.Request) error {
63
+	// if we're redirected with an access token in the anchor, use it to set our transport to a proper bearer auth
64
+	if matches := accessTokenRedirectRegex.FindAllStringSubmatch(req.URL.String(), 1); matches != nil {
65
+		tokenGetter.accessToken = matches[0][1]
66
+	}
67
+
68
+	if len(via) >= 10 {
69
+		return errors.New("stopped after 10 redirects")
70
+	}
71
+
72
+	return nil
73
+}
74
+
75
+// challengingClient conforms the kclient.HTTPClient interface.  It introspects responses for auth challenges and
76
+// tries to response to those challenges in order to get a token back.
77
+type challengingClient struct {
78
+	delegate *http.Client
79
+	reader   io.Reader
80
+}
81
+
82
+const (
83
+	basicAuthPattern = `[\s]*Basic[\s]*realm="([\w]+)"`
84
+)
85
+
86
+var (
87
+	basicAuthRegex = regexp.MustCompile(basicAuthPattern)
88
+)
89
+
90
+// Do watches for unauthorized challenges.  If we know to respond, we respond to the challenge
91
+func (client *challengingClient) Do(req *http.Request) (*http.Response, error) {
92
+	resp, err := client.delegate.Do(req)
93
+	if resp.StatusCode == http.StatusUnauthorized {
94
+		if wantsBasicAuth, realm := isBasicAuthChallenge(resp); wantsBasicAuth {
95
+			fmt.Printf("Authenticate for \"%v\"\n", realm)
96
+			username := promptForString("username", client.reader)
97
+			password := promptForString("password", client.reader)
98
+
99
+			client.delegate.Transport = kclient.NewBasicAuthRoundTripper(username, password, client.delegate.Transport)
100
+			return client.Do(resp.Request)
101
+		}
102
+	}
103
+	return resp, err
104
+}
105
+
106
+func isBasicAuthChallenge(resp *http.Response) (bool, string) {
107
+	for currHeader, headerValue := range resp.Header {
108
+		if strings.EqualFold(currHeader, "WWW-Authenticate") {
109
+			for _, currAuthorizeHeader := range headerValue {
110
+				if matches := basicAuthRegex.FindAllStringSubmatch(currAuthorizeHeader, 1); matches != nil {
111
+					return true, matches[0][1]
112
+				}
113
+			}
114
+		}
115
+	}
116
+
117
+	return false, ""
118
+}
119
+
120
+func promptForString(field string, r io.Reader) string {
121
+	fmt.Printf("Please enter %s: ", field)
122
+	var result string
123
+	fmt.Fscan(r, &result)
124
+	return result
125
+}
... ...
@@ -67,6 +67,9 @@ type Client struct {
67 67
 	// Secret is the unique secret associated with a client
68 68
 	Secret string `json:"secret,omitempty" yaml:"secret,omitempty"`
69 69
 
70
+	// RespondWithChallenges indicates whether the client wants authentication needed responses made in the form of challenges instead of redirects
71
+	RespondWithChallenges bool `json:"respondWithChallenges,omitempty" yaml:"respondWithChallenges,omitempty"`
72
+
70 73
 	// RedirectURIs is the valid redirection URIs associated with a client
71 74
 	RedirectURIs []string `json:"redirectURIs,omitempty" yaml:"redirectURIs,omitempty"`
72 75
 }
... ...
@@ -67,6 +67,9 @@ type Client struct {
67 67
 	// Secret is the unique secret associated with a client
68 68
 	Secret string `json:"secret,omitempty" yaml:"secret,omitempty"`
69 69
 
70
+	// RespondWithChallenges indicates whether the client wants authentication needed responses made in the form of challenges instead of redirects
71
+	RespondWithChallenges bool `json:"respondWithChallenges,omitempty" yaml:"respondWithChallenges,omitempty"`
72
+
70 73
 	// RedirectURIs is the valid redirection URIs associated with a client
71 74
 	RedirectURIs []string `json:"redirectURIs,omitempty" yaml:"redirectURIs,omitempty"`
72 75
 }
73 76
new file mode 100644
... ...
@@ -0,0 +1,17 @@
0
+// osintypes holds types that are used to interpret responses from the RangelReale osin server.
1
+// neither osin, nor osincli contain types with the annotations required to have the default json
2
+// marshaller encode and decode them.  Even more unusual: osin does not describe a single type that
3
+// represents the overall return from osin.FinishInfoRequest.  Because of that, a type needs to be
4
+// described in order to make use of the return value, so even if you preferred writing a parser
5
+// you'll end up needing a type anyway.
6
+package osintypes
7
+
8
+// InfoResponseData is a type that matches the information returned from osin.FinishInfoRequest (/oauth/info).
9
+type InfoResponseData struct {
10
+	Error            string `json:"error" yaml:"error"`
11
+	ErrorDescription string `json:"error_description" yaml:"error_description"`
12
+	TokenType        string `json:"token_type" yaml:"token_type"`
13
+	AccessToken      string `json:"access_token" yaml:"access_token"`
14
+	RefreshToken     string `json:"refresh_token" yaml:"refresh_token"`
15
+	Expiration       int32  `json:"expires_in" yaml:"expires_in"`
16
+}
... ...
@@ -60,7 +60,7 @@ func (w *clientWrapper) GetRedirectUri() string {
60 60
 }
61 61
 
62 62
 func (w *clientWrapper) GetUserData() interface{} {
63
-	return nil
63
+	return w.client
64 64
 }
65 65
 
66 66
 // Clone the storage if needed. For example, using mgo, you can clone the session with session.Clone
... ...
@@ -10,6 +10,12 @@ type Info interface {
10 10
 	GetUID() string
11 11
 }
12 12
 
13
+// mux is an object that can register http handlers.
14
+type mux interface {
15
+	Handle(pattern string, handler http.Handler)
16
+	HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
17
+}
18
+
13 19
 type Context interface {
14 20
 	Get(*http.Request) (Info, bool)
15 21
 }
... ...
@@ -38,3 +44,20 @@ func NewCurrentContextFilter(requestPath string, context Context, handler http.H
38 38
 		handler.ServeHTTP(w, req)
39 39
 	})
40 40
 }
41
+
42
+// InstallThisUser registers the APIServer log support function into a mux.
43
+func InstallThisUser(mux mux, endpoint string, requestsToUsers Context, apiHandler http.Handler) {
44
+	mux.HandleFunc(endpoint,
45
+		func(w http.ResponseWriter, req *http.Request) {
46
+			user, found := requestsToUsers.Get(req)
47
+			if !found {
48
+				http.Error(w, "Need to be authorized to access this method", http.StatusUnauthorized)
49
+				return
50
+			}
51
+
52
+			base := path.Dir(req.URL.Path)
53
+			req.URL.Path = path.Join(base, user.GetName())
54
+			apiHandler.ServeHTTP(w, req)
55
+		},
56
+	)
57
+}
41 58
new file mode 100644
... ...
@@ -0,0 +1,111 @@
0
+// +build integration,!no-etcd
1
+
2
+package integration
3
+
4
+import (
5
+	"bytes"
6
+	"net/http"
7
+	"net/http/httptest"
8
+	"strings"
9
+	"testing"
10
+
11
+	"github.com/golang/glog"
12
+	"github.com/spf13/pflag"
13
+
14
+	klatest "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
15
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/master"
16
+
17
+	// for osinserver setup.
18
+	"github.com/openshift/origin/pkg/auth/authenticator/anyauthpassword"
19
+	authhandlers "github.com/openshift/origin/pkg/auth/authenticator/handlers"
20
+	"github.com/openshift/origin/pkg/auth/authenticator/requesthandlers"
21
+	oauthhandlers "github.com/openshift/origin/pkg/auth/oauth/handlers"
22
+	oauthregistry "github.com/openshift/origin/pkg/auth/oauth/registry"
23
+	"github.com/openshift/origin/pkg/auth/userregistry/identitymapper"
24
+	"github.com/openshift/origin/pkg/cmd/server/origin"
25
+	oauthetcd "github.com/openshift/origin/pkg/oauth/registry/etcd"
26
+	"github.com/openshift/origin/pkg/oauth/server/osinserver"
27
+	"github.com/openshift/origin/pkg/oauth/server/osinserver/registrystorage"
28
+	"github.com/openshift/origin/pkg/user"
29
+	useretcd "github.com/openshift/origin/pkg/user/registry/etcd"
30
+
31
+	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
32
+	"github.com/openshift/origin/pkg/cmd/util/tokencmd"
33
+)
34
+
35
+func init() {
36
+	requireEtcd()
37
+}
38
+
39
+func TestGetToken(t *testing.T) {
40
+	deleteAllEtcdKeys()
41
+
42
+	// setup
43
+	etcdClient := newEtcdClient()
44
+	etcdHelper, _ := master.NewEtcdHelper(etcdClient, klatest.Version)
45
+	oauthEtcd := oauthetcd.New(etcdHelper)
46
+	userRegistry := useretcd.New(etcdHelper, user.NewDefaultUserInitStrategy())
47
+	identityMapper := identitymapper.NewAlwaysCreateUserIdentityToUserMapper("front-proxy-test" /*for now*/, userRegistry)
48
+
49
+	authRequestHandler := requesthandlers.NewBasicAuthAuthentication(anyauthpassword.New(identityMapper))
50
+	authHandler := oauthhandlers.NewUnionAuthenticationHandler(
51
+		map[string]oauthhandlers.AuthenticationChallenger{"login": authhandlers.NewBasicAuthChallenger("openshift")}, nil, nil)
52
+
53
+	storage := registrystorage.New(oauthEtcd, oauthEtcd, oauthEtcd, oauthregistry.NewUserConversion())
54
+	config := osinserver.NewDefaultServerConfig()
55
+
56
+	grantChecker := oauthregistry.NewClientAuthorizationGrantChecker(oauthEtcd)
57
+	grantHandler := oauthhandlers.NewAutoGrant(oauthEtcd)
58
+
59
+	server := osinserver.New(
60
+		config,
61
+		storage,
62
+		osinserver.AuthorizeHandlers{
63
+			oauthhandlers.NewAuthorizeAuthenticator(
64
+				authRequestHandler,
65
+				authHandler,
66
+				oauthhandlers.EmptyError{},
67
+			),
68
+			oauthhandlers.NewGrantCheck(
69
+				grantChecker,
70
+				grantHandler,
71
+				oauthhandlers.EmptyError{},
72
+			),
73
+		},
74
+		osinserver.AccessHandlers{
75
+			oauthhandlers.NewDenyAccessAuthenticator(),
76
+		},
77
+		osinserver.NewDefaultErrorHandler(),
78
+	)
79
+	mux := http.NewServeMux()
80
+	server.Install(mux, origin.OpenShiftOAuthAPIPrefix)
81
+	oauthServer := httptest.NewServer(http.Handler(mux))
82
+	glog.Infof("oauth server is on %v\n", oauthServer.URL)
83
+
84
+	// create the default oauth clients with redirects to our server
85
+	origin.CreateOrUpdateDefaultOAuthClients(oauthServer.URL, oauthEtcd)
86
+
87
+	flags := pflag.NewFlagSet("test-flags", pflag.ContinueOnError)
88
+	clientCfg := clientcmd.NewConfig()
89
+	clientCfg.Bind(flags)
90
+	flags.Parse(strings.Split("--master="+oauthServer.URL, " "))
91
+
92
+	reader := bytes.NewBufferString("user\npass")
93
+	accessToken, err := tokencmd.RequestToken(clientCfg, reader)
94
+
95
+	if err != nil {
96
+		t.Errorf("Unexpected error: %v", err)
97
+	}
98
+	if len(accessToken) == 0 {
99
+		t.Error("Expected accessToken, but did not get one")
100
+	}
101
+
102
+	// lets see if this access token is any good
103
+	token, err := oauthEtcd.GetAccessToken(accessToken)
104
+	if err != nil {
105
+		t.Errorf("Unexpected error: %v", err)
106
+	}
107
+	if token.UserName != "front-proxy-test:user" {
108
+		t.Errorf("Expected token for \"user\", but got: %#v", token)
109
+	}
110
+}