Merge pull request #567 from ericchiang/dev-html-templates
dev branch: port templates from master branch
This commit is contained in:
		
							
								
								
									
										5
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								Makefile
									
									
									
									
									
								
							@@ -19,7 +19,7 @@ GOARCH=$(shell go env GOARCH)
 | 
			
		||||
 | 
			
		||||
build: bin/dex bin/example-app
 | 
			
		||||
 | 
			
		||||
bin/dex: FORCE
 | 
			
		||||
bin/dex: FORCE server/templates_default.go
 | 
			
		||||
	@go install -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex
 | 
			
		||||
 | 
			
		||||
bin/example-app: FORCE
 | 
			
		||||
@@ -42,6 +42,9 @@ lint:
 | 
			
		||||
      golint $$package; \
 | 
			
		||||
	done
 | 
			
		||||
 | 
			
		||||
server/templates_default.go: $(wildcard web/templates/**)
 | 
			
		||||
	@go run server/templates_default_gen.go
 | 
			
		||||
 | 
			
		||||
.PHONY: docker-build
 | 
			
		||||
docker-build: bin/dex
 | 
			
		||||
	@docker build -t $(DOCKER_IMAGE) .
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import (
 | 
			
		||||
	"github.com/coreos/dex/connector/ldap"
 | 
			
		||||
	"github.com/coreos/dex/connector/mock"
 | 
			
		||||
	"github.com/coreos/dex/connector/oidc"
 | 
			
		||||
	"github.com/coreos/dex/server"
 | 
			
		||||
	"github.com/coreos/dex/storage"
 | 
			
		||||
	"github.com/coreos/dex/storage/kubernetes"
 | 
			
		||||
	"github.com/coreos/dex/storage/memory"
 | 
			
		||||
@@ -21,6 +22,8 @@ type Config struct {
 | 
			
		||||
	Web        Web         `yaml:"web"`
 | 
			
		||||
	OAuth2     OAuth2      `yaml:"oauth2"`
 | 
			
		||||
 | 
			
		||||
	Templates server.TemplateConfig `yaml:"templates"`
 | 
			
		||||
 | 
			
		||||
	StaticClients []storage.Client `yaml:"staticClients"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -111,9 +114,15 @@ func (c *Connector) UnmarshalYAML(unmarshal func(interface{}) error) error {
 | 
			
		||||
 | 
			
		||||
	var err error
 | 
			
		||||
	switch c.Type {
 | 
			
		||||
	case "mock":
 | 
			
		||||
	case "mockCallback":
 | 
			
		||||
		var config struct {
 | 
			
		||||
			Config mock.Config `yaml:"config"`
 | 
			
		||||
			Config mock.CallbackConfig `yaml:"config"`
 | 
			
		||||
		}
 | 
			
		||||
		err = unmarshal(&config)
 | 
			
		||||
		c.Config = &config.Config
 | 
			
		||||
	case "mockPassword":
 | 
			
		||||
		var config struct {
 | 
			
		||||
			Config mock.PasswordConfig `yaml:"config"`
 | 
			
		||||
		}
 | 
			
		||||
		err = unmarshal(&config)
 | 
			
		||||
		c.Config = &config.Config
 | 
			
		||||
 
 | 
			
		||||
@@ -89,11 +89,11 @@ func serve(cmd *cobra.Command, args []string) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	serverConfig := server.Config{
 | 
			
		||||
		Issuer:     c.Issuer,
 | 
			
		||||
		Connectors: connectors,
 | 
			
		||||
		Storage:    s,
 | 
			
		||||
 | 
			
		||||
		SupportedResponseTypes: c.OAuth2.ResponseTypes,
 | 
			
		||||
		Issuer:                 c.Issuer,
 | 
			
		||||
		Connectors:             connectors,
 | 
			
		||||
		Storage:                s,
 | 
			
		||||
		TemplateConfig:         c.Templates,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	serv, err := server.New(serverConfig)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
// Package mock implements a mock connector which requires no user interaction.
 | 
			
		||||
// Package mock implements connectors which help test various server components.
 | 
			
		||||
package mock
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
@@ -11,22 +11,24 @@ import (
 | 
			
		||||
	"github.com/coreos/dex/connector"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// New returns a mock connector which requires no user interaction. It always returns
 | 
			
		||||
// NewCallbackConnector returns a mock connector which requires no user interaction. It always returns
 | 
			
		||||
// the same (fake) identity.
 | 
			
		||||
func New() connector.Connector {
 | 
			
		||||
	return mockConnector{}
 | 
			
		||||
func NewCallbackConnector() connector.Connector {
 | 
			
		||||
	return callbackConnector{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	_ connector.CallbackConnector = mockConnector{}
 | 
			
		||||
	_ connector.GroupsConnector   = mockConnector{}
 | 
			
		||||
	_ connector.CallbackConnector = callbackConnector{}
 | 
			
		||||
	_ connector.GroupsConnector   = callbackConnector{}
 | 
			
		||||
 | 
			
		||||
	_ connector.PasswordConnector = passwordConnector{}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type mockConnector struct{}
 | 
			
		||||
type callbackConnector struct{}
 | 
			
		||||
 | 
			
		||||
func (m mockConnector) Close() error { return nil }
 | 
			
		||||
func (m callbackConnector) Close() error { return nil }
 | 
			
		||||
 | 
			
		||||
func (m mockConnector) LoginURL(callbackURL, state string) (string, error) {
 | 
			
		||||
func (m callbackConnector) LoginURL(callbackURL, state string) (string, error) {
 | 
			
		||||
	u, err := url.Parse(callbackURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("failed to parse callbackURL %q: %v", callbackURL, err)
 | 
			
		||||
@@ -39,7 +41,7 @@ func (m mockConnector) LoginURL(callbackURL, state string) (string, error) {
 | 
			
		||||
 | 
			
		||||
var connectorData = []byte("foobar")
 | 
			
		||||
 | 
			
		||||
func (m mockConnector) HandleCallback(r *http.Request) (connector.Identity, string, error) {
 | 
			
		||||
func (m callbackConnector) HandleCallback(r *http.Request) (connector.Identity, string, error) {
 | 
			
		||||
	return connector.Identity{
 | 
			
		||||
		UserID:        "0-385-28089-0",
 | 
			
		||||
		Username:      "Kilgore Trout",
 | 
			
		||||
@@ -49,17 +51,54 @@ func (m mockConnector) HandleCallback(r *http.Request) (connector.Identity, stri
 | 
			
		||||
	}, r.URL.Query().Get("state"), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m mockConnector) Groups(identity connector.Identity) ([]string, error) {
 | 
			
		||||
func (m callbackConnector) Groups(identity connector.Identity) ([]string, error) {
 | 
			
		||||
	if !bytes.Equal(identity.ConnectorData, connectorData) {
 | 
			
		||||
		return nil, errors.New("connector data mismatch")
 | 
			
		||||
	}
 | 
			
		||||
	return []string{"authors"}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Config holds the configuration parameters for the mock connector.
 | 
			
		||||
type Config struct{}
 | 
			
		||||
// CallbackConfig holds the configuration parameters for a connector which requires no interaction.
 | 
			
		||||
type CallbackConfig struct{}
 | 
			
		||||
 | 
			
		||||
// Open returns an authentication strategy which requires no user interaction.
 | 
			
		||||
func (c *Config) Open() (connector.Connector, error) {
 | 
			
		||||
	return New(), nil
 | 
			
		||||
func (c *CallbackConfig) Open() (connector.Connector, error) {
 | 
			
		||||
	return NewCallbackConnector(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PasswordConfig holds the configuration for a mock connector which prompts for the supplied
 | 
			
		||||
// username and password.
 | 
			
		||||
type PasswordConfig struct {
 | 
			
		||||
	Username string `yaml:"username"`
 | 
			
		||||
	Password string `yaml:"password"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Open returns an authentication strategy which prompts for a predefined username and password.
 | 
			
		||||
func (c *PasswordConfig) Open() (connector.Connector, error) {
 | 
			
		||||
	if c.Username == "" {
 | 
			
		||||
		return nil, errors.New("no username supplied")
 | 
			
		||||
	}
 | 
			
		||||
	if c.Password == "" {
 | 
			
		||||
		return nil, errors.New("no password supplied")
 | 
			
		||||
	}
 | 
			
		||||
	return &passwordConnector{c.Username, c.Password}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type passwordConnector struct {
 | 
			
		||||
	username string
 | 
			
		||||
	password string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p passwordConnector) Close() error { return nil }
 | 
			
		||||
 | 
			
		||||
func (p passwordConnector) Login(username, password string) (identity connector.Identity, validPassword bool, err error) {
 | 
			
		||||
	if username == p.username && password == p.password {
 | 
			
		||||
		return connector.Identity{
 | 
			
		||||
			UserID:        "0-385-28089-0",
 | 
			
		||||
			Username:      "Kilgore Trout",
 | 
			
		||||
			Email:         "kilgore@kilgore.trout",
 | 
			
		||||
			EmailVerified: true,
 | 
			
		||||
		}, true, nil
 | 
			
		||||
	}
 | 
			
		||||
	return identity, false, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,25 +7,15 @@ web:
 | 
			
		||||
  http: 127.0.0.1:5556
 | 
			
		||||
 | 
			
		||||
connectors:
 | 
			
		||||
- type: mock
 | 
			
		||||
  id: mock
 | 
			
		||||
- type: mockCallback
 | 
			
		||||
  id: mock-callback
 | 
			
		||||
  name: Mock
 | 
			
		||||
- type: github
 | 
			
		||||
  id: github
 | 
			
		||||
  name: GitHub
 | 
			
		||||
- type: mockPassword
 | 
			
		||||
  id: mock-password
 | 
			
		||||
  name: Password
 | 
			
		||||
  config:
 | 
			
		||||
    clientID: "$GITHUB_CLIENT_ID"
 | 
			
		||||
    clientSecret: "$GITHUB_CLIENT_SECRET"
 | 
			
		||||
    redirectURI: http://127.0.0.1:5556/callback/github
 | 
			
		||||
    org: kubernetes
 | 
			
		||||
- type: oidc
 | 
			
		||||
  id: google
 | 
			
		||||
  name: Google Account
 | 
			
		||||
  config:
 | 
			
		||||
    issuer: https://accounts.google.com
 | 
			
		||||
    clientID: "$GOOGLE_OAUTH2_CLIENT_ID"
 | 
			
		||||
    clientSecret: "$GOOGLE_OAUTH2_CLIENT_SECRET"
 | 
			
		||||
    redirectURI: http://127.0.0.1:5556/callback/google
 | 
			
		||||
    username: "admin"
 | 
			
		||||
    password: "PASSWORD"
 | 
			
		||||
 | 
			
		||||
staticClients:
 | 
			
		||||
- id: example-app
 | 
			
		||||
 
 | 
			
		||||
@@ -129,15 +129,16 @@ func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
 | 
			
		||||
	connectorInfos := make([]connectorInfo, len(s.connectors))
 | 
			
		||||
	i := 0
 | 
			
		||||
	for id := range s.connectors {
 | 
			
		||||
	for id, conn := range s.connectors {
 | 
			
		||||
		connectorInfos[i] = connectorInfo{
 | 
			
		||||
			DisplayName: id,
 | 
			
		||||
			URL:         s.absPath("/auth", id),
 | 
			
		||||
			ID:   id,
 | 
			
		||||
			Name: conn.DisplayName,
 | 
			
		||||
			URL:  s.absPath("/auth", id),
 | 
			
		||||
		}
 | 
			
		||||
		i++
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	renderLoginOptions(w, connectorInfos, state)
 | 
			
		||||
	s.templates.login(w, connectorInfos, state)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
@@ -163,7 +164,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
			}
 | 
			
		||||
			http.Redirect(w, r, callbackURL, http.StatusFound)
 | 
			
		||||
		case connector.PasswordConnector:
 | 
			
		||||
			renderPasswordTmpl(w, state, r.URL.String(), "")
 | 
			
		||||
			s.templates.password(w, state, r.URL.String(), "", false)
 | 
			
		||||
		default:
 | 
			
		||||
			s.notFound(w, r)
 | 
			
		||||
		}
 | 
			
		||||
@@ -174,7 +175,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		username := r.FormValue("username")
 | 
			
		||||
		username := r.FormValue("login")
 | 
			
		||||
		password := r.FormValue("password")
 | 
			
		||||
 | 
			
		||||
		identity, ok, err := passwordConnector.Login(username, password)
 | 
			
		||||
@@ -184,7 +185,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		if !ok {
 | 
			
		||||
			renderPasswordTmpl(w, state, r.URL.String(), "Invalid credentials")
 | 
			
		||||
			s.templates.password(w, state, r.URL.String(), username, true)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		redirectURL, err := s.finalizeLogin(identity, state, connID, conn.Connector)
 | 
			
		||||
@@ -299,7 +300,7 @@ func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
			s.renderError(w, http.StatusInternalServerError, errServerError, "")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		renderApprovalTmpl(w, authReq.ID, *authReq.Claims, client, authReq.Scopes)
 | 
			
		||||
		s.templates.approval(w, authReq.ID, authReq.Claims.Username, client.Name, authReq.Scopes)
 | 
			
		||||
	case "POST":
 | 
			
		||||
		if r.FormValue("approval") != "approve" {
 | 
			
		||||
			s.renderError(w, http.StatusInternalServerError, "approval rejected", "")
 | 
			
		||||
 
 | 
			
		||||
@@ -43,6 +43,8 @@ type Config struct {
 | 
			
		||||
 | 
			
		||||
	// If specified, the server will use this function for determining time.
 | 
			
		||||
	Now func() time.Time
 | 
			
		||||
 | 
			
		||||
	TemplateConfig TemplateConfig
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func value(val, defaultValue time.Duration) time.Duration {
 | 
			
		||||
@@ -63,6 +65,8 @@ type Server struct {
 | 
			
		||||
 | 
			
		||||
	mux http.Handler
 | 
			
		||||
 | 
			
		||||
	templates *templates
 | 
			
		||||
 | 
			
		||||
	// If enabled, don't prompt user for approval after logging in through connector.
 | 
			
		||||
	// No package level API to set this, only used in tests.
 | 
			
		||||
	skipApproval bool
 | 
			
		||||
@@ -107,6 +111,11 @@ func newServer(c Config, rotationStrategy rotationStrategy) (*Server, error) {
 | 
			
		||||
		supported[respType] = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tmpls, err := loadTemplates(c.TemplateConfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("server: failed to load templates: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	now := c.Now
 | 
			
		||||
	if now == nil {
 | 
			
		||||
		now = time.Now
 | 
			
		||||
@@ -124,6 +133,7 @@ func newServer(c Config, rotationStrategy rotationStrategy) (*Server, error) {
 | 
			
		||||
		supportedResponseTypes: supported,
 | 
			
		||||
		idTokensValidFor:       value(c.IDTokensValidFor, 24*time.Hour),
 | 
			
		||||
		now:                    now,
 | 
			
		||||
		templates:              tmpls,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, conn := range c.Connectors {
 | 
			
		||||
 
 | 
			
		||||
@@ -64,7 +64,7 @@ FDWV28nTP9sqbtsmU8Tem2jzMvZ7C/Q0AuDoKELFUpux8shm8wfIhyaPnXUGZoAZ
 | 
			
		||||
Np4vUwMSYV5mopESLWOg3loBxKyLGFtgGKVCjGiQvy6zISQ4fQo=
 | 
			
		||||
-----END RSA PRIVATE KEY-----`)
 | 
			
		||||
 | 
			
		||||
func newTestServer(updateConfig func(c *Config)) (*httptest.Server, *Server) {
 | 
			
		||||
func newTestServer(t *testing.T, updateConfig func(c *Config)) (*httptest.Server, *Server) {
 | 
			
		||||
	var server *Server
 | 
			
		||||
	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		server.ServeHTTP(w, r)
 | 
			
		||||
@@ -76,7 +76,7 @@ func newTestServer(updateConfig func(c *Config)) (*httptest.Server, *Server) {
 | 
			
		||||
			{
 | 
			
		||||
				ID:          "mock",
 | 
			
		||||
				DisplayName: "Mock",
 | 
			
		||||
				Connector:   mock.New(),
 | 
			
		||||
				Connector:   mock.NewCallbackConnector(),
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
@@ -87,21 +87,21 @@ func newTestServer(updateConfig func(c *Config)) (*httptest.Server, *Server) {
 | 
			
		||||
 | 
			
		||||
	var err error
 | 
			
		||||
	if server, err = newServer(config, staticRotationStrategy(testKey)); err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	server.skipApproval = true // Don't prompt for approval, just immediately redirect with code.
 | 
			
		||||
	return s, server
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestNewTestServer(t *testing.T) {
 | 
			
		||||
	newTestServer(nil)
 | 
			
		||||
	newTestServer(t, nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDiscovery(t *testing.T) {
 | 
			
		||||
	ctx, cancel := context.WithCancel(context.Background())
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	httpServer, _ := newTestServer(func(c *Config) {
 | 
			
		||||
	httpServer, _ := newTestServer(t, func(c *Config) {
 | 
			
		||||
		c.Issuer = c.Issuer + "/non-root-path"
 | 
			
		||||
	})
 | 
			
		||||
	defer httpServer.Close()
 | 
			
		||||
@@ -129,7 +129,7 @@ func TestOAuth2CodeFlow(t *testing.T) {
 | 
			
		||||
	ctx, cancel := context.WithCancel(context.Background())
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	httpServer, s := newTestServer(func(c *Config) {
 | 
			
		||||
	httpServer, s := newTestServer(t, func(c *Config) {
 | 
			
		||||
		c.Issuer = c.Issuer + "/non-root-path"
 | 
			
		||||
	})
 | 
			
		||||
	defer httpServer.Close()
 | 
			
		||||
@@ -255,7 +255,7 @@ func TestOAuth2ImplicitFlow(t *testing.T) {
 | 
			
		||||
	ctx, cancel := context.WithCancel(context.Background())
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	httpServer, s := newTestServer(func(c *Config) {
 | 
			
		||||
	httpServer, s := newTestServer(t, func(c *Config) {
 | 
			
		||||
		// Enable support for the implicit flow.
 | 
			
		||||
		c.SupportedResponseTypes = []string{"code", "token"}
 | 
			
		||||
	})
 | 
			
		||||
 
 | 
			
		||||
@@ -1,101 +1,196 @@
 | 
			
		||||
package server
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"text/template"
 | 
			
		||||
 | 
			
		||||
	"github.com/coreos/dex/storage"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type connectorInfo struct {
 | 
			
		||||
	DisplayName string
 | 
			
		||||
	URL         string
 | 
			
		||||
const (
 | 
			
		||||
	tmplApproval = "approval.html"
 | 
			
		||||
	tmplLogin    = "login.html"
 | 
			
		||||
	tmplPassword = "password.html"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const coreOSLogoURL = "https://coreos.com/assets/images/brand/coreos-wordmark-135x40px.png"
 | 
			
		||||
 | 
			
		||||
var requiredTmpls = []string{
 | 
			
		||||
	tmplApproval,
 | 
			
		||||
	tmplLogin,
 | 
			
		||||
	tmplPassword,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var loginTmpl = template.Must(template.New("login-template").Parse(`<html>
 | 
			
		||||
<head></head>
 | 
			
		||||
<body>
 | 
			
		||||
<p>Login options</p>
 | 
			
		||||
{{ range $i, $connector := .Connectors }}
 | 
			
		||||
<a href="{{ $connector.URL }}?state={{ $.State }}">{{ $connector.DisplayName }}</a>
 | 
			
		||||
{{ end }}
 | 
			
		||||
</body>
 | 
			
		||||
</html>`))
 | 
			
		||||
// TemplateConfig describes.
 | 
			
		||||
type TemplateConfig struct {
 | 
			
		||||
	// Directory of the templates. If empty, these will be loaded from memory.
 | 
			
		||||
	Dir string `yaml:"dir"`
 | 
			
		||||
 | 
			
		||||
	// Defaults to the CoreOS logo and "dex".
 | 
			
		||||
	LogoURL string `yaml:"logoURL"`
 | 
			
		||||
	Issuer  string `yaml:"issuerName"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type globalData struct {
 | 
			
		||||
	LogoURL string
 | 
			
		||||
	Issuer  string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func loadTemplates(config TemplateConfig) (*templates, error) {
 | 
			
		||||
	var tmpls *template.Template
 | 
			
		||||
	if config.Dir != "" {
 | 
			
		||||
		files, err := ioutil.ReadDir(config.Dir)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("read dir: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		filenames := []string{}
 | 
			
		||||
		for _, file := range files {
 | 
			
		||||
			if file.IsDir() {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			filenames = append(filenames, filepath.Join(config.Dir, file.Name()))
 | 
			
		||||
		}
 | 
			
		||||
		if len(filenames) == 0 {
 | 
			
		||||
			return nil, fmt.Errorf("no files in template dir %s", config.Dir)
 | 
			
		||||
		}
 | 
			
		||||
		if tmpls, err = template.ParseFiles(filenames...); err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("parse files: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		// Load templates from memory. This code is largely copied from the standard library's
 | 
			
		||||
		// ParseFiles source code.
 | 
			
		||||
		// See: https://goo.gl/6Wm4mN
 | 
			
		||||
		for name, data := range defaultTemplates {
 | 
			
		||||
			var t *template.Template
 | 
			
		||||
			if tmpls == nil {
 | 
			
		||||
				tmpls = template.New(name)
 | 
			
		||||
			}
 | 
			
		||||
			if name == tmpls.Name() {
 | 
			
		||||
				t = tmpls
 | 
			
		||||
			} else {
 | 
			
		||||
				t = tmpls.New(name)
 | 
			
		||||
			}
 | 
			
		||||
			if _, err := t.Parse(data); err != nil {
 | 
			
		||||
				return nil, fmt.Errorf("parsing %s: %v", name, err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	missingTmpls := []string{}
 | 
			
		||||
	for _, tmplName := range requiredTmpls {
 | 
			
		||||
		if tmpls.Lookup(tmplName) == nil {
 | 
			
		||||
			missingTmpls = append(missingTmpls, tmplName)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if len(missingTmpls) > 0 {
 | 
			
		||||
		return nil, fmt.Errorf("missing template(s): %s", missingTmpls)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if config.LogoURL == "" {
 | 
			
		||||
		config.LogoURL = coreOSLogoURL
 | 
			
		||||
	}
 | 
			
		||||
	if config.Issuer == "" {
 | 
			
		||||
		config.Issuer = "dex"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &templates{
 | 
			
		||||
		globalData:   config,
 | 
			
		||||
		loginTmpl:    tmpls.Lookup(tmplLogin),
 | 
			
		||||
		approvalTmpl: tmpls.Lookup(tmplApproval),
 | 
			
		||||
		passwordTmpl: tmpls.Lookup(tmplPassword),
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var scopeDescriptions = map[string]string{
 | 
			
		||||
	"offline_access": "Have offline access",
 | 
			
		||||
	"profile":        "View basic profile information",
 | 
			
		||||
	"email":          "View your email",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type templates struct {
 | 
			
		||||
	globalData   TemplateConfig
 | 
			
		||||
	loginTmpl    *template.Template
 | 
			
		||||
	approvalTmpl *template.Template
 | 
			
		||||
	passwordTmpl *template.Template
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type connectorInfo struct {
 | 
			
		||||
	ID   string
 | 
			
		||||
	Name string
 | 
			
		||||
	URL  string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type byName []connectorInfo
 | 
			
		||||
 | 
			
		||||
func (n byName) Len() int           { return len(n) }
 | 
			
		||||
func (n byName) Less(i, j int) bool { return n[i].Name < n[j].Name }
 | 
			
		||||
func (n byName) Swap(i, j int)      { n[i], n[j] = n[j], n[i] }
 | 
			
		||||
 | 
			
		||||
func (t *templates) login(w http.ResponseWriter, connectors []connectorInfo, state string) {
 | 
			
		||||
	sort.Sort(byName(connectors))
 | 
			
		||||
 | 
			
		||||
func renderLoginOptions(w http.ResponseWriter, connectors []connectorInfo, state string) {
 | 
			
		||||
	data := struct {
 | 
			
		||||
		TemplateConfig
 | 
			
		||||
		Connectors []connectorInfo
 | 
			
		||||
		State      string
 | 
			
		||||
	}{connectors, state}
 | 
			
		||||
	renderTemplate(w, loginTmpl, data)
 | 
			
		||||
	}{t.globalData, connectors, state}
 | 
			
		||||
	renderTemplate(w, t.loginTmpl, data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var passwordTmpl = template.Must(template.New("password-template").Parse(`<html>
 | 
			
		||||
<body>
 | 
			
		||||
<p>Login</p>
 | 
			
		||||
<form action="{{ .Callback }}" method="POST">
 | 
			
		||||
Login: <input type="text" name="login"/><br/>
 | 
			
		||||
Password: <input type="password" name="password"/><br/>
 | 
			
		||||
<input type="hidden" name="state" value="{{ .State }}"/>
 | 
			
		||||
<input type="submit"/>
 | 
			
		||||
{{ if .Message }}
 | 
			
		||||
<p>Error: {{ .Message }}</p>
 | 
			
		||||
{{ end }}
 | 
			
		||||
</form>
 | 
			
		||||
</body>
 | 
			
		||||
</html>`))
 | 
			
		||||
 | 
			
		||||
func renderPasswordTmpl(w http.ResponseWriter, state, callback, message string) {
 | 
			
		||||
func (t *templates) password(w http.ResponseWriter, state, callback, lastUsername string, lastWasInvalid bool) {
 | 
			
		||||
	data := struct {
 | 
			
		||||
		TemplateConfig
 | 
			
		||||
		State    string
 | 
			
		||||
		Callback string
 | 
			
		||||
		Message  string
 | 
			
		||||
	}{state, callback, message}
 | 
			
		||||
	renderTemplate(w, passwordTmpl, data)
 | 
			
		||||
		PostURL  string
 | 
			
		||||
		Username string
 | 
			
		||||
		Invalid  bool
 | 
			
		||||
	}{t.globalData, state, callback, lastUsername, lastWasInvalid}
 | 
			
		||||
	renderTemplate(w, t.passwordTmpl, data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var approvalTmpl = template.Must(template.New("approval-template").Parse(`<html>
 | 
			
		||||
<body>
 | 
			
		||||
<p>User: {{ .User }}</p>
 | 
			
		||||
<p>Client: {{ .ClientName }}</p>
 | 
			
		||||
<form method="post">
 | 
			
		||||
<input type="hidden" name="state" value="{{ .State }}"/>
 | 
			
		||||
<input type="hidden" name="approval" value="approve">
 | 
			
		||||
<button type="submit">Approve</button>
 | 
			
		||||
</form>
 | 
			
		||||
<form method="post">
 | 
			
		||||
<input type="hidden" name="state" value="{{ .State }}"/>
 | 
			
		||||
<input type="hidden" name="approval" value="reject">
 | 
			
		||||
<button type="submit">Reject</button>
 | 
			
		||||
</form>
 | 
			
		||||
</body>
 | 
			
		||||
</html>`))
 | 
			
		||||
 | 
			
		||||
func renderApprovalTmpl(w http.ResponseWriter, state string, identity storage.Claims, client storage.Client, scopes []string) {
 | 
			
		||||
func (t *templates) approval(w http.ResponseWriter, state, username, clientName string, scopes []string) {
 | 
			
		||||
	accesses := []string{}
 | 
			
		||||
	for _, scope := range scopes {
 | 
			
		||||
		access, ok := scopeDescriptions[scope]
 | 
			
		||||
		if ok {
 | 
			
		||||
			accesses = append(accesses, access)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	sort.Strings(accesses)
 | 
			
		||||
	data := struct {
 | 
			
		||||
		User       string
 | 
			
		||||
		ClientName string
 | 
			
		||||
		State      string
 | 
			
		||||
	}{identity.Email, client.Name, state}
 | 
			
		||||
	renderTemplate(w, approvalTmpl, data)
 | 
			
		||||
		TemplateConfig
 | 
			
		||||
		User   string
 | 
			
		||||
		Client string
 | 
			
		||||
		State  string
 | 
			
		||||
		Scopes []string
 | 
			
		||||
	}{t.globalData, username, clientName, state, accesses}
 | 
			
		||||
	renderTemplate(w, t.approvalTmpl, data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// small io.Writer utilitiy to determine if executing the template wrote to the underlying response writer.
 | 
			
		||||
type writeRecorder struct {
 | 
			
		||||
	wrote bool
 | 
			
		||||
	w     io.Writer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (w *writeRecorder) Write(p []byte) (n int, err error) {
 | 
			
		||||
	w.wrote = true
 | 
			
		||||
	return w.w.Write(p)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func renderTemplate(w http.ResponseWriter, tmpl *template.Template, data interface{}) {
 | 
			
		||||
	err := tmpl.Execute(w, data)
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch err := err.(type) {
 | 
			
		||||
	case template.ExecError:
 | 
			
		||||
		// An ExecError guarentees that Execute has not written to the underlying reader.
 | 
			
		||||
	wr := &writeRecorder{w: w}
 | 
			
		||||
	if err := tmpl.Execute(wr, data); err != nil {
 | 
			
		||||
		log.Printf("Error rendering template %s: %s", tmpl.Name(), err)
 | 
			
		||||
 | 
			
		||||
		// TODO(ericchiang): replace with better internal server error.
 | 
			
		||||
		http.Error(w, "Internal server error", http.StatusInternalServerError)
 | 
			
		||||
	default:
 | 
			
		||||
		// An error with the underlying write, such as the connection being
 | 
			
		||||
		// dropped. Ignore for now.
 | 
			
		||||
		if !wr.wrote {
 | 
			
		||||
			// TODO(ericchiang): replace with better internal server error.
 | 
			
		||||
			http.Error(w, "Internal server error", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								server/templates_default.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								server/templates_default.go
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										79
									
								
								server/templates_default_gen.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								server/templates_default_gen.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
			
		||||
// +build ignore
 | 
			
		||||
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ignoreFile uses "git check-ignore" to determine if we should ignore a file.
 | 
			
		||||
func ignoreFile(p string) (ok bool, err error) {
 | 
			
		||||
	err = exec.Command("git", "check-ignore", p).Run()
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		return true, nil
 | 
			
		||||
	}
 | 
			
		||||
	exitErr, ok := err.(*exec.ExitError)
 | 
			
		||||
	if ok {
 | 
			
		||||
		if sys := exitErr.Sys(); sys != nil {
 | 
			
		||||
			e, ok := sys.(interface {
 | 
			
		||||
				// Is the returned value something that returns an exit status?
 | 
			
		||||
				ExitStatus() int
 | 
			
		||||
			})
 | 
			
		||||
			if ok && e.ExitStatus() == 1 {
 | 
			
		||||
				return false, nil
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type fileData struct {
 | 
			
		||||
	name string
 | 
			
		||||
	data string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	dir, err := ioutil.ReadDir("web/templates")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	files := []fileData{}
 | 
			
		||||
	for _, file := range dir {
 | 
			
		||||
		p := filepath.Join("web/templates", file.Name())
 | 
			
		||||
		ignore, err := ignoreFile(p)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		if ignore {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		data, err := ioutil.ReadFile(p)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		files = append(files, fileData{file.Name(), string(data)})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	f := new(bytes.Buffer)
 | 
			
		||||
 | 
			
		||||
	fmt.Fprintln(f, "// This file was generated by the makefile. Do not edit.")
 | 
			
		||||
	fmt.Fprintln(f)
 | 
			
		||||
	fmt.Fprintln(f, "package server")
 | 
			
		||||
	fmt.Fprintln(f)
 | 
			
		||||
	fmt.Fprintln(f, "// defaultTemplates is a key for file name to file data of the files in web/templates.")
 | 
			
		||||
	fmt.Fprintln(f, "var defaultTemplates = map[string]string{")
 | 
			
		||||
	for _, file := range files {
 | 
			
		||||
		fmt.Fprintf(f, "\t%q: %q,\n", file.name, file.data)
 | 
			
		||||
	}
 | 
			
		||||
	fmt.Fprintln(f, "}")
 | 
			
		||||
 | 
			
		||||
	if err := ioutil.WriteFile("server/templates_default.go", f.Bytes(), 0644); err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1 +1,16 @@
 | 
			
		||||
package server
 | 
			
		||||
 | 
			
		||||
import "testing"
 | 
			
		||||
 | 
			
		||||
func TestNewTemplates(t *testing.T) {
 | 
			
		||||
	var config TemplateConfig
 | 
			
		||||
	if _, err := loadTemplates(config); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestLoadTemplates(t *testing.T) {
 | 
			
		||||
	var config TemplateConfig
 | 
			
		||||
 | 
			
		||||
	config.Dir = "../web/templates"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								web/templates/approval.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								web/templates/approval.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
{{ template "header.html" . }}
 | 
			
		||||
 | 
			
		||||
<div class="panel">
 | 
			
		||||
  <h2 class="heading">Grant Access</h2>
 | 
			
		||||
 | 
			
		||||
  <hr>
 | 
			
		||||
  <div class="list-with-title">
 | 
			
		||||
    <div class="subtle-text">{{ .Client }} would like to:</div>
 | 
			
		||||
      {{ range $scope := .Scopes }}
 | 
			
		||||
      <li class="bullet-point">
 | 
			
		||||
        <div class="subtle-text">
 | 
			
		||||
          {{ $scope }}
 | 
			
		||||
        </div>
 | 
			
		||||
      </li>
 | 
			
		||||
      {{ end }}
 | 
			
		||||
  </div>
 | 
			
		||||
  <hr>
 | 
			
		||||
 | 
			
		||||
  <div>
 | 
			
		||||
    <div class="form-row">
 | 
			
		||||
      <form method="post">
 | 
			
		||||
        <input type="hidden" name="state" value="{{ .State }}"/>
 | 
			
		||||
        <input type="hidden" name="approval" value="approve">
 | 
			
		||||
        <button type="submit" class="btn btn-success">
 | 
			
		||||
            <span class="btn-text">Grant Access</span>
 | 
			
		||||
        </button>
 | 
			
		||||
      </form>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="form-row">
 | 
			
		||||
      <form method="post">
 | 
			
		||||
        <input type="hidden" name="state" value="{{ .State }}"/>
 | 
			
		||||
        <input type="hidden" name="approval" value="rejected">
 | 
			
		||||
        <button type="submit" class="btn btn-provider">
 | 
			
		||||
            <span class="btn-text">Cancel</span>
 | 
			
		||||
        </button>
 | 
			
		||||
      </form>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{{ template "footer.html" . }}
 | 
			
		||||
							
								
								
									
										3
									
								
								web/templates/footer.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								web/templates/footer.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
    </div>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										240
									
								
								web/templates/header.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								web/templates/header.html
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										22
									
								
								web/templates/login.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								web/templates/login.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
{{ template "header.html" . }}
 | 
			
		||||
 | 
			
		||||
<div class="panel">
 | 
			
		||||
  <h2 class="heading">Log in to {{ .Issuer }} </h2>
 | 
			
		||||
 | 
			
		||||
  <div>
 | 
			
		||||
    {{ range $c := .Connectors }}
 | 
			
		||||
      <div class="form-row">
 | 
			
		||||
        <a href="{{ $c.URL }}?state={{ $.State }}" target="_self">
 | 
			
		||||
          <button class="btn btn-provider">
 | 
			
		||||
            <span class="btn-icon btn-icon-{{ $c.ID }}"></span>
 | 
			
		||||
            <span class="btn-text">Log in with {{ $c.Name }}</span>
 | 
			
		||||
          </button>
 | 
			
		||||
        </a>
 | 
			
		||||
      </div>
 | 
			
		||||
    {{ end }}
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{{ template "footer.html" . }}
 | 
			
		||||
							
								
								
									
										31
									
								
								web/templates/password.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								web/templates/password.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
{{ template "header.html" . }}
 | 
			
		||||
 | 
			
		||||
<div class="panel">
 | 
			
		||||
  <h2 class="heading">Log in to Your Account</h2>
 | 
			
		||||
  <form method="post" action="{{ .PostURL }}">
 | 
			
		||||
    <div class="form-row">
 | 
			
		||||
      <div class="input-desc">
 | 
			
		||||
        <label for="userid">Username</label>
 | 
			
		||||
      </div>
 | 
			
		||||
	  <input tabindex="1" required id="login" name="login" type="text" class="input-box" placeholder="username" {{ if .Username }}value="{{ .Username }}" {{ else }} autofocus {{ end }}/>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="form-row">
 | 
			
		||||
      <div class="input-desc">
 | 
			
		||||
        <label for="password">Password</label>
 | 
			
		||||
      </div>
 | 
			
		||||
	  <input tabindex="2" required id="password" name="password" type="password" class="input-box" placeholder="password" {{ if .Invalid }} autofocus {{ end }}/>
 | 
			
		||||
    </div>
 | 
			
		||||
    <input type="hidden" name="state" value="{{ .State }}"/>
 | 
			
		||||
 | 
			
		||||
    {{ if .Invalid }}
 | 
			
		||||
      <div class="error-box">
 | 
			
		||||
        Invalid username and password.
 | 
			
		||||
      </div>
 | 
			
		||||
    {{ end }}
 | 
			
		||||
 | 
			
		||||
    <button tabindex="3" type="submit" class="btn btn-primary">Login</button>
 | 
			
		||||
 | 
			
		||||
  </form>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{{ template "footer.html" . }}
 | 
			
		||||
		Reference in New Issue
	
	Block a user