package integration import ( "bytes" "crypto/tls" "io/ioutil" "net/http" "net/http/httptest" "net/url" "os" "reflect" "testing" "k8s.io/kubernetes/pkg/client/restclient" clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" "github.com/openshift/origin/pkg/client" "github.com/openshift/origin/pkg/cmd/cli/cmd" "github.com/openshift/origin/pkg/cmd/cli/cmd/login" configapi "github.com/openshift/origin/pkg/cmd/server/api" testutil "github.com/openshift/origin/test/util" testserver "github.com/openshift/origin/test/util/server" ) // TestOAuthOIDC checks CLI password login against an OIDC provider func TestOAuthOIDC(t *testing.T) { expectedTokenPost := url.Values{ "grant_type": []string{"password"}, "client_id": []string{"myclient"}, "client_secret": []string{"mysecret"}, "username": []string{"mylogin"}, "password": []string{"mypassword"}, "scope": []string{"openid scope1 scope2"}, } // id_token made at https://jwt.io/ // { // "sub": "mysub", // "name": "John Doe", // "myidclaim": "myid", // "myemailclaim":"myemail", // } tokenResponse := `{ "token_type": "bearer", "access_token": "12345", "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJteXN1YiIsIm5hbWUiOiJKb2huIERvZSIsIm15aWRjbGFpbSI6Im15aWQiLCJteWVtYWlsY2xhaW0iOiJteWVtYWlsIn0.yMx2ZQw8Su641H_kO8ec_tFaysrFEc9uFUFm4ZbLGHw" }` // Additional claims in userInfo (sub claim must match) userinfoResponse := `{ "sub": "mysub", "mynameclaim":"myname", "myusernameclaim":"myusername" }` // Write cert we're going to use to verify OIDC server requests caFile, err := ioutil.TempFile("", "test.crt") if err != nil { t.Fatalf("unexpected error: %v", err) } defer os.Remove(caFile.Name()) if err := ioutil.WriteFile(caFile.Name(), oidcLocalhostCert, os.FileMode(0600)); err != nil { t.Fatalf("unexpected error: %v", err) } // Get master config testutil.RequireEtcd(t) defer testutil.DumpEtcdOnFailure(t) masterOptions, err := testserver.DefaultMasterOptions() if err != nil { t.Fatalf("unexpected error: %v", err) } // Set up a dummy OIDC server oidcServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.String() { case "/token": if r.Method != "POST" { t.Fatalf("Expected POST to /token, got %s", r.Method) } if err := r.ParseForm(); err != nil { t.Fatalf("Error parsing form POSTed to /token: %v", err) } if !reflect.DeepEqual(r.PostForm, expectedTokenPost) { t.Fatalf("Expected\n%#v\ngot\n%#v", expectedTokenPost, r.PostForm) } w.Write([]byte(tokenResponse)) case "/userinfo": if r.Header.Get("Authorization") != "Bearer 12345" { t.Fatalf("Expected authorization header, got %#v", r.Header) } w.Write([]byte(userinfoResponse)) default: t.Fatalf("Unexpected OIDC request: %v", r.URL.String()) } })) cert, err := tls.X509KeyPair(oidcLocalhostCert, oidcLocalhostKey) oidcServer.TLS = &tls.Config{ Certificates: []tls.Certificate{cert}, } oidcServer.StartTLS() defer oidcServer.Close() masterOptions.OAuthConfig.IdentityProviders[0] = configapi.IdentityProvider{ Name: "oidc", UseAsChallenger: true, UseAsLogin: true, MappingMethod: "claim", Provider: &configapi.OpenIDIdentityProvider{ CA: caFile.Name(), ClientID: "myclient", ClientSecret: configapi.StringSource{StringSourceSpec: configapi.StringSourceSpec{Value: "mysecret"}}, ExtraScopes: []string{"scope1", "scope2"}, URLs: configapi.OpenIDURLs{ Authorize: oidcServer.URL + "/authorize", Token: oidcServer.URL + "/token", UserInfo: oidcServer.URL + "/userinfo", }, Claims: configapi.OpenIDClaims{ ID: []string{"myidclaim"}, Email: []string{"myemailclaim"}, Name: []string{"mynameclaim"}, PreferredUsername: []string{"myusernameclaim"}, }, }, } // Start server clusterAdminKubeConfig, err := testserver.StartConfiguredMaster(masterOptions) if err != nil { t.Fatalf("unexpected error: %v", err) } clientConfig, err := testutil.GetClusterAdminClientConfig(clusterAdminKubeConfig) if err != nil { t.Fatalf("unexpected error: %v", err) } // Get the master CA data for the login command masterCAFile := clientConfig.CAFile if masterCAFile == "" { // Write master ca data tmpFile, err := ioutil.TempFile("", "ca.crt") if err != nil { t.Fatalf("unexpected error: %v", err) } defer os.Remove(tmpFile.Name()) if err := ioutil.WriteFile(tmpFile.Name(), clientConfig.CAData, os.FileMode(0600)); err != nil { t.Fatalf("unexpected error: %v", err) } masterCAFile = tmpFile.Name() } // Attempt a login using a redirecting auth proxy loginOutput := &bytes.Buffer{} loginOptions := &login.LoginOptions{ Server: clientConfig.Host, CAFile: masterCAFile, StartingKubeConfig: &clientcmdapi.Config{}, Reader: bytes.NewBufferString("mylogin\nmypassword\n"), Out: loginOutput, } if err := loginOptions.GatherInfo(); err != nil { t.Fatalf("Error logging in: %v\n%v", err, loginOutput.String()) } if loginOptions.Username != "myusername" { t.Fatalf("Unexpected user after authentication: %#v", loginOptions) } if len(loginOptions.Config.BearerToken) == 0 { t.Fatalf("Expected token after authentication: %#v", loginOptions.Config) } // Ex userConfig := &restclient.Config{ Host: clientConfig.Host, TLSClientConfig: restclient.TLSClientConfig{ CAFile: clientConfig.CAFile, CAData: clientConfig.CAData, }, BearerToken: loginOptions.Config.BearerToken, } userClient, err := client.New(userConfig) userWhoamiOptions := cmd.WhoAmIOptions{UserInterface: userClient.Users(), Out: ioutil.Discard} retrievedUser, err := userWhoamiOptions.WhoAmI() if err != nil { t.Errorf("unexpected error: %v", err) } if retrievedUser.Name != "myusername" { t.Errorf("expected username %v, got %v", "myusername", retrievedUser.Name) } if retrievedUser.FullName != "myname" { t.Errorf("expected display name %v, got %v", "myname", retrievedUser.FullName) } if !reflect.DeepEqual([]string{"oidc:myid"}, retrievedUser.Identities) { t.Errorf("expected only oidc:myid identity, got %v", retrievedUser.Identities) } } // oidcLocalhostCert is a PEM-encoded TLS cert with SAN IPs // "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. // generated from src/crypto/tls: // go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h var oidcLocalhostCert = []byte(`-----BEGIN CERTIFICATE----- MIICEzCCAXygAwIBAgIQMIMChMLGrR+QvmQvpwAU6zANBgkqhkiG9w0BAQsFADAS MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB iQKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9SjY1bIw4 iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZBl2+XsDul rKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQABo2gwZjAO BgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUw AwEB/zAuBgNVHREEJzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAA AAAAATANBgkqhkiG9w0BAQsFAAOBgQCEcetwO59EWk7WiJsG4x8SY+UIAA+flUI9 tyC4lNhbcF2Idq9greZwbYCqTTTr2XiRNSMLCOjKyI7ukPoPjo16ocHj+P3vZGfs h1fIw3cSS2OolhloGw/XM6RWPWtPAlGykKLciQrBru5NAPvCMsb/I1DAceTiotQM fblo6RBxUQ== -----END CERTIFICATE-----`) // oidcLocalhostKey is the private key for oidcLocalhostCert. var oidcLocalhostKey = []byte(`-----BEGIN RSA PRIVATE KEY----- MIICXgIBAAKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9 SjY1bIw4iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZB l2+XsDulrKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQAB AoGAGRzwwir7XvBOAy5tM/uV6e+Zf6anZzus1s1Y1ClbjbE6HXbnWWF/wbZGOpet 3Zm4vD6MXc7jpTLryzTQIvVdfQbRc6+MUVeLKwZatTXtdZrhu+Jk7hx0nTPy8Jcb uJqFk541aEw+mMogY/xEcfbWd6IOkp+4xqjlFLBEDytgbIECQQDvH/E6nk+hgN4H qzzVtxxr397vWrjrIgPbJpQvBsafG7b0dA4AFjwVbFLmQcj2PprIMmPcQrooz8vp jy4SHEg1AkEA/v13/5M47K9vCxmb8QeD/asydfsgS5TeuNi8DoUBEmiSJwma7FXY fFUtxuvL7XvjwjN5B30pNEbc6Iuyt7y4MQJBAIt21su4b3sjXNueLKH85Q+phy2U fQtuUE9txblTu14q3N7gHRZB4ZMhFYyDy8CKrN2cPg/Fvyt0Xlp/DoCzjA0CQQDU y2ptGsuSmgUtWj3NM9xuwYPm+Z/F84K6+ARYiZ6PYj013sovGKUFfYAqVXVlxtIX qyUBnu3X9ps8ZfjLZO7BAkEAlT4R5Yl6cGhaJQYZHOde3JEMhNRcVFMO8dJDaFeo f9Oeos0UUothgiDktdQHxdNEwLjQf7lJJBzV+5OtwswCWA== -----END RSA PRIVATE KEY-----`)