Fix spelling errors in docs

This commit is contained in:
Chandan Rai 2019-10-03 10:49:18 +01:00 committed by Chandan Rai
commit efdb5de6d8
8 changed files with 187 additions and 68 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
bin bin
dist dist
_output _output
.idea

View file

@ -89,7 +89,7 @@ connectors:
# server provides access for anonymous auth. # server provides access for anonymous auth.
# Please note that if the bind password contains a `$`, it has to be saved in an # Please note that if the bind password contains a `$`, it has to be saved in an
# environment variable which should be given as the value to `bindPW`. # environment variable which should be given as the value to `bindPW`.
bindDN: uid=seviceaccount,cn=users,dc=example,dc=com bindDN: uid=serviceaccount,cn=users,dc=example,dc=com
bindPW: password bindPW: password
# The attribute to display in the provided password prompt. If unset, will # The attribute to display in the provided password prompt. If unset, will

View file

@ -10,7 +10,7 @@ Prominent examples of OpenID Connect providers include Google Accounts, Salesfor
This connector does not support the "groups" claim. Progress for this is tracked in [issue #1065][issue-1065]. This connector does not support the "groups" claim. Progress for this is tracked in [issue #1065][issue-1065].
When using refresh tokens, changes to the upstream claims aren't propegated to the id_token returned by dex. If a user's email changes, the "email" claim returned by dex won't change unless the user logs in again. Progress for this is tracked in [issue #863][issue-863]. When using refresh tokens, changes to the upstream claims aren't propagated to the id_token returned by dex. If a user's email changes, the "email" claim returned by dex won't change unless the user logs in again. Progress for this is tracked in [issue #863][issue-863].
## Configuration ## Configuration
@ -36,7 +36,7 @@ connectors:
# Some providers require passing client_secret via POST parameters instead # Some providers require passing client_secret via POST parameters instead
# of basic auth, despite the OAuth2 RFC discouraging it. Many of these # of basic auth, despite the OAuth2 RFC discouraging it. Many of these
# cases are caught internally, but some may need to uncommented the # cases are caught internally, but some may need to uncomment the
# following field. # following field.
# #
# basicAuthUnsupported: true # basicAuthUnsupported: true
@ -56,7 +56,7 @@ connectors:
# - email # - email
# - groups # - groups
# Some providers return claims without "email_verified", when they had no usage of emails verification in enrollement process # Some providers return claims without "email_verified", when they had no usage of emails verification in enrollment process
# or if they are acting as a proxy for another IDP etc AWS Cognito with an upstream SAML IDP # or if they are acting as a proxy for another IDP etc AWS Cognito with an upstream SAML IDP
# This can be overridden with the below option # This can be overridden with the below option
# insecureSkipEmailVerified: true # insecureSkipEmailVerified: true

View file

@ -11,7 +11,7 @@ in with GitHub.
## The problem ## The problem
When dex is federaing to an upstream identity provider (IDP), we want to ensure When dex is federating to an upstream identity provider (IDP), we want to ensure
claims being passed onto clients remain fresh. This includes data such as Google claims being passed onto clients remain fresh. This includes data such as Google
accounts display names, LDAP group membership, account deactivations. Changes to accounts display names, LDAP group membership, account deactivations. Changes to
these on an upstream IDP should always be reflected in the claims dex passes to these on an upstream IDP should always be reflected in the claims dex passes to

View file

@ -101,7 +101,7 @@ func (h *healthChecker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.mu.RUnlock() h.mu.RUnlock()
if err != nil { if err != nil {
h.s.renderError(w, http.StatusInternalServerError, "Health check failed.") h.s.renderError(r, w, http.StatusInternalServerError, "Health check failed.")
return return
} }
fmt.Fprintf(w, "Health check passed in %s", t) fmt.Fprintf(w, "Health check passed in %s", t)
@ -112,13 +112,13 @@ func (s *Server) handlePublicKeys(w http.ResponseWriter, r *http.Request) {
keys, err := s.storage.GetKeys() keys, err := s.storage.GetKeys()
if err != nil { if err != nil {
s.logger.Errorf("failed to get keys: %v", err) s.logger.Errorf("failed to get keys: %v", err)
s.renderError(w, http.StatusInternalServerError, "Internal server error.") s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
return return
} }
if keys.SigningKeyPub == nil { if keys.SigningKeyPub == nil {
s.logger.Errorf("No public keys found.") s.logger.Errorf("No public keys found.")
s.renderError(w, http.StatusInternalServerError, "Internal server error.") s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
return return
} }
@ -133,7 +133,7 @@ func (s *Server) handlePublicKeys(w http.ResponseWriter, r *http.Request) {
data, err := json.MarshalIndent(jwks, "", " ") data, err := json.MarshalIndent(jwks, "", " ")
if err != nil { if err != nil {
s.logger.Errorf("failed to marshal discovery data: %v", err) s.logger.Errorf("failed to marshal discovery data: %v", err)
s.renderError(w, http.StatusInternalServerError, "Internal server error.") s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
return return
} }
maxAge := keys.NextRotation.Sub(s.now()) maxAge := keys.NextRotation.Sub(s.now())
@ -214,7 +214,7 @@ func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
status = err.Status() status = err.Status()
} }
s.renderError(w, status, err.Error()) s.renderError(r, w, status, err.Error())
return return
} }
@ -226,14 +226,14 @@ func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
authReq.Expiry = s.now().Add(s.authRequestsValidFor) authReq.Expiry = s.now().Add(s.authRequestsValidFor)
if err := s.storage.CreateAuthRequest(*authReq); err != nil { if err := s.storage.CreateAuthRequest(*authReq); err != nil {
s.logger.Errorf("Failed to create authorization request: %v", err) s.logger.Errorf("Failed to create authorization request: %v", err)
s.renderError(w, http.StatusInternalServerError, "Failed to connect to the database.") s.renderError(r, w, http.StatusInternalServerError, "Failed to connect to the database.")
return return
} }
connectors, err := s.storage.ListConnectors() connectors, err := s.storage.ListConnectors()
if err != nil { if err != nil {
s.logger.Errorf("Failed to get list of connectors: %v", err) s.logger.Errorf("Failed to get list of connectors: %v", err)
s.renderError(w, http.StatusInternalServerError, "Failed to retrieve connector list.") s.renderError(r, w, http.StatusInternalServerError, "Failed to retrieve connector list.")
return return
} }
@ -271,7 +271,7 @@ func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
i++ i++
} }
if err := s.templates.login(w, connectorInfos); err != nil { if err := s.templates.login(r, w, connectorInfos, r.URL.Path); err != nil {
s.logger.Errorf("Server template error: %v", err) s.logger.Errorf("Server template error: %v", err)
} }
} }
@ -281,7 +281,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
conn, err := s.getConnector(connID) conn, err := s.getConnector(connID)
if err != nil { if err != nil {
s.logger.Errorf("Failed to create authorization request: %v", err) s.logger.Errorf("Failed to create authorization request: %v", err)
s.renderError(w, http.StatusBadRequest, "Requested resource does not exist") s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist")
return return
} }
@ -291,9 +291,9 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
s.logger.Errorf("Failed to get auth request: %v", err) s.logger.Errorf("Failed to get auth request: %v", err)
if err == storage.ErrNotFound { if err == storage.ErrNotFound {
s.renderError(w, http.StatusBadRequest, "Login session expired.") s.renderError(r, w, http.StatusBadRequest, "Login session expired.")
} else { } else {
s.renderError(w, http.StatusInternalServerError, "Database error.") s.renderError(r, w, http.StatusInternalServerError, "Database error.")
} }
return return
} }
@ -306,7 +306,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
} }
if err := s.storage.UpdateAuthRequest(authReqID, updater); err != nil { if err := s.storage.UpdateAuthRequest(authReqID, updater); err != nil {
s.logger.Errorf("Failed to set connector ID on auth request: %v", err) s.logger.Errorf("Failed to set connector ID on auth request: %v", err)
s.renderError(w, http.StatusInternalServerError, "Database error.") s.renderError(r, w, http.StatusInternalServerError, "Database error.")
return return
} }
} }
@ -324,19 +324,19 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
callbackURL, err := conn.LoginURL(scopes, s.absURL("/callback"), authReqID) callbackURL, err := conn.LoginURL(scopes, s.absURL("/callback"), authReqID)
if err != nil { if err != nil {
s.logger.Errorf("Connector %q returned error when creating callback: %v", connID, err) s.logger.Errorf("Connector %q returned error when creating callback: %v", connID, err)
s.renderError(w, http.StatusInternalServerError, "Login error.") s.renderError(r, w, http.StatusInternalServerError, "Login error.")
return return
} }
http.Redirect(w, r, callbackURL, http.StatusFound) http.Redirect(w, r, callbackURL, http.StatusFound)
case connector.PasswordConnector: case connector.PasswordConnector:
if err := s.templates.password(w, r.URL.String(), "", usernamePrompt(conn), false, showBacklink); err != nil { if err := s.templates.password(r, w, r.URL.String(), "", usernamePrompt(conn), false, showBacklink, r.URL.Path); err != nil {
s.logger.Errorf("Server template error: %v", err) s.logger.Errorf("Server template error: %v", err)
} }
case connector.SAMLConnector: case connector.SAMLConnector:
action, value, err := conn.POSTData(scopes, authReqID) action, value, err := conn.POSTData(scopes, authReqID)
if err != nil { if err != nil {
s.logger.Errorf("Creating SAML data: %v", err) s.logger.Errorf("Creating SAML data: %v", err)
s.renderError(w, http.StatusInternalServerError, "Connector Login Error") s.renderError(r, w, http.StatusInternalServerError, "Connector Login Error")
return return
} }
@ -358,12 +358,12 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
</body> </body>
</html>`, action, value, authReqID) </html>`, action, value, authReqID)
default: default:
s.renderError(w, http.StatusBadRequest, "Requested resource does not exist.") s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist.")
} }
case http.MethodPost: case http.MethodPost:
passwordConnector, ok := conn.Connector.(connector.PasswordConnector) passwordConnector, ok := conn.Connector.(connector.PasswordConnector)
if !ok { if !ok {
s.renderError(w, http.StatusBadRequest, "Requested resource does not exist.") s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist.")
return return
} }
@ -373,11 +373,11 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
identity, ok, err := passwordConnector.Login(r.Context(), scopes, username, password) identity, ok, err := passwordConnector.Login(r.Context(), scopes, username, password)
if err != nil { if err != nil {
s.logger.Errorf("Failed to login user: %v", err) s.logger.Errorf("Failed to login user: %v", err)
s.renderError(w, http.StatusInternalServerError, fmt.Sprintf("Login error: %v", err)) s.renderError(r, w, http.StatusInternalServerError, fmt.Sprintf("Login error: %v", err))
return return
} }
if !ok { if !ok {
if err := s.templates.password(w, r.URL.String(), username, usernamePrompt(passwordConnector), true, showBacklink); err != nil { if err := s.templates.password(r, w, r.URL.String(), username, usernamePrompt(passwordConnector), true, showBacklink, r.URL.Path); err != nil {
s.logger.Errorf("Server template error: %v", err) s.logger.Errorf("Server template error: %v", err)
} }
return return
@ -385,13 +385,13 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
redirectURL, err := s.finalizeLogin(identity, authReq, conn.Connector) redirectURL, err := s.finalizeLogin(identity, authReq, conn.Connector)
if err != nil { if err != nil {
s.logger.Errorf("Failed to finalize login: %v", err) s.logger.Errorf("Failed to finalize login: %v", err)
s.renderError(w, http.StatusInternalServerError, "Login error.") s.renderError(r, w, http.StatusInternalServerError, "Login error.")
return return
} }
http.Redirect(w, r, redirectURL, http.StatusSeeOther) http.Redirect(w, r, redirectURL, http.StatusSeeOther)
default: default:
s.renderError(w, http.StatusBadRequest, "Unsupported request method.") s.renderError(r, w, http.StatusBadRequest, "Unsupported request method.")
} }
} }
@ -400,16 +400,16 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
switch r.Method { switch r.Method {
case http.MethodGet: // OAuth2 callback case http.MethodGet: // OAuth2 callback
if authID = r.URL.Query().Get("state"); authID == "" { if authID = r.URL.Query().Get("state"); authID == "" {
s.renderError(w, http.StatusBadRequest, "User session error.") s.renderError(r, w, http.StatusBadRequest, "User session error.")
return return
} }
case http.MethodPost: // SAML POST binding case http.MethodPost: // SAML POST binding
if authID = r.PostFormValue("RelayState"); authID == "" { if authID = r.PostFormValue("RelayState"); authID == "" {
s.renderError(w, http.StatusBadRequest, "User session error.") s.renderError(r, w, http.StatusBadRequest, "User session error.")
return return
} }
default: default:
s.renderError(w, http.StatusBadRequest, "Method not supported") s.renderError(r, w, http.StatusBadRequest, "Method not supported")
return return
} }
@ -417,24 +417,24 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
if err != nil { if err != nil {
if err == storage.ErrNotFound { if err == storage.ErrNotFound {
s.logger.Errorf("Invalid 'state' parameter provided: %v", err) s.logger.Errorf("Invalid 'state' parameter provided: %v", err)
s.renderError(w, http.StatusBadRequest, "Requested resource does not exist.") s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist.")
return return
} }
s.logger.Errorf("Failed to get auth request: %v", err) s.logger.Errorf("Failed to get auth request: %v", err)
s.renderError(w, http.StatusInternalServerError, "Database error.") s.renderError(r, w, http.StatusInternalServerError, "Database error.")
return return
} }
if connID := mux.Vars(r)["connector"]; connID != "" && connID != authReq.ConnectorID { if connID := mux.Vars(r)["connector"]; connID != "" && connID != authReq.ConnectorID {
s.logger.Errorf("Connector mismatch: authentication started with id %q, but callback for id %q was triggered", authReq.ConnectorID, connID) s.logger.Errorf("Connector mismatch: authentication started with id %q, but callback for id %q was triggered", authReq.ConnectorID, connID)
s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.") s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
return return
} }
conn, err := s.getConnector(authReq.ConnectorID) conn, err := s.getConnector(authReq.ConnectorID)
if err != nil { if err != nil {
s.logger.Errorf("Failed to get connector with id %q : %v", authReq.ConnectorID, err) s.logger.Errorf("Failed to get connector with id %q : %v", authReq.ConnectorID, err)
s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.") s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
return return
} }
@ -443,32 +443,32 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
case connector.CallbackConnector: case connector.CallbackConnector:
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
s.logger.Errorf("SAML request mapped to OAuth2 connector") s.logger.Errorf("SAML request mapped to OAuth2 connector")
s.renderError(w, http.StatusBadRequest, "Invalid request") s.renderError(r, w, http.StatusBadRequest, "Invalid request")
return return
} }
identity, err = conn.HandleCallback(parseScopes(authReq.Scopes), r) identity, err = conn.HandleCallback(parseScopes(authReq.Scopes), r)
case connector.SAMLConnector: case connector.SAMLConnector:
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
s.logger.Errorf("OAuth2 request mapped to SAML connector") s.logger.Errorf("OAuth2 request mapped to SAML connector")
s.renderError(w, http.StatusBadRequest, "Invalid request") s.renderError(r, w, http.StatusBadRequest, "Invalid request")
return return
} }
identity, err = conn.HandlePOST(parseScopes(authReq.Scopes), r.PostFormValue("SAMLResponse"), authReq.ID) identity, err = conn.HandlePOST(parseScopes(authReq.Scopes), r.PostFormValue("SAMLResponse"), authReq.ID)
default: default:
s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.") s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
return return
} }
if err != nil { if err != nil {
s.logger.Errorf("Failed to authenticate: %v", err) s.logger.Errorf("Failed to authenticate: %v", err)
s.renderError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to authenticate: %v", err)) s.renderError(r, w, http.StatusInternalServerError, fmt.Sprintf("Failed to authenticate: %v", err))
return return
} }
redirectURL, err := s.finalizeLogin(identity, authReq, conn.Connector) redirectURL, err := s.finalizeLogin(identity, authReq, conn.Connector)
if err != nil { if err != nil {
s.logger.Errorf("Failed to finalize login: %v", err) s.logger.Errorf("Failed to finalize login: %v", err)
s.renderError(w, http.StatusInternalServerError, "Login error.") s.renderError(r, w, http.StatusInternalServerError, "Login error.")
return return
} }
@ -511,12 +511,12 @@ func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
authReq, err := s.storage.GetAuthRequest(r.FormValue("req")) authReq, err := s.storage.GetAuthRequest(r.FormValue("req"))
if err != nil { if err != nil {
s.logger.Errorf("Failed to get auth request: %v", err) s.logger.Errorf("Failed to get auth request: %v", err)
s.renderError(w, http.StatusInternalServerError, "Database error.") s.renderError(r, w, http.StatusInternalServerError, "Database error.")
return return
} }
if !authReq.LoggedIn { if !authReq.LoggedIn {
s.logger.Errorf("Auth request does not have an identity for approval") s.logger.Errorf("Auth request does not have an identity for approval")
s.renderError(w, http.StatusInternalServerError, "Login process not yet finalized.") s.renderError(r, w, http.StatusInternalServerError, "Login process not yet finalized.")
return return
} }
@ -529,15 +529,15 @@ func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
client, err := s.storage.GetClient(authReq.ClientID) client, err := s.storage.GetClient(authReq.ClientID)
if err != nil { if err != nil {
s.logger.Errorf("Failed to get client %q: %v", authReq.ClientID, err) s.logger.Errorf("Failed to get client %q: %v", authReq.ClientID, err)
s.renderError(w, http.StatusInternalServerError, "Failed to retrieve client.") s.renderError(r, w, http.StatusInternalServerError, "Failed to retrieve client.")
return return
} }
if err := s.templates.approval(w, authReq.ID, authReq.Claims.Username, client.Name, authReq.Scopes); err != nil { if err := s.templates.approval(r, w, authReq.ID, authReq.Claims.Username, client.Name, authReq.Scopes, r.URL.Path); err != nil {
s.logger.Errorf("Server template error: %v", err) s.logger.Errorf("Server template error: %v", err)
} }
case http.MethodPost: case http.MethodPost:
if r.FormValue("approval") != "approve" { if r.FormValue("approval") != "approve" {
s.renderError(w, http.StatusInternalServerError, "Approval rejected.") s.renderError(r, w, http.StatusInternalServerError, "Approval rejected.")
return return
} }
s.sendCodeResponse(w, r, authReq) s.sendCodeResponse(w, r, authReq)
@ -546,22 +546,22 @@ func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authReq storage.AuthRequest) { func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authReq storage.AuthRequest) {
if s.now().After(authReq.Expiry) { if s.now().After(authReq.Expiry) {
s.renderError(w, http.StatusBadRequest, "User session has expired.") s.renderError(r, w, http.StatusBadRequest, "User session has expired.")
return return
} }
if err := s.storage.DeleteAuthRequest(authReq.ID); err != nil { if err := s.storage.DeleteAuthRequest(authReq.ID); err != nil {
if err != storage.ErrNotFound { if err != storage.ErrNotFound {
s.logger.Errorf("Failed to delete authorization request: %v", err) s.logger.Errorf("Failed to delete authorization request: %v", err)
s.renderError(w, http.StatusInternalServerError, "Internal server error.") s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
} else { } else {
s.renderError(w, http.StatusBadRequest, "User session error.") s.renderError(r, w, http.StatusBadRequest, "User session error.")
} }
return return
} }
u, err := url.Parse(authReq.RedirectURI) u, err := url.Parse(authReq.RedirectURI)
if err != nil { if err != nil {
s.renderError(w, http.StatusInternalServerError, "Invalid redirect URI.") s.renderError(r, w, http.StatusInternalServerError, "Invalid redirect URI.")
return return
} }
@ -598,14 +598,14 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
} }
if err := s.storage.CreateAuthCode(code); err != nil { if err := s.storage.CreateAuthCode(code); err != nil {
s.logger.Errorf("Failed to create auth code: %v", err) s.logger.Errorf("Failed to create auth code: %v", err)
s.renderError(w, http.StatusInternalServerError, "Internal server error.") s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
return return
} }
// Implicit and hybrid flows that try to use the OOB redirect URI are // Implicit and hybrid flows that try to use the OOB redirect URI are
// rejected earlier. If we got here we're using the code flow. // rejected earlier. If we got here we're using the code flow.
if authReq.RedirectURI == redirectURIOOB { if authReq.RedirectURI == redirectURIOOB {
if err := s.templates.oob(w, code.ID); err != nil { if err := s.templates.oob(r, w, code.ID, r.URL.Path); err != nil {
s.logger.Errorf("Server template error: %v", err) s.logger.Errorf("Server template error: %v", err)
} }
return return
@ -1119,8 +1119,8 @@ func (s *Server) writeAccessToken(w http.ResponseWriter, idToken, accessToken, r
w.Write(data) w.Write(data)
} }
func (s *Server) renderError(w http.ResponseWriter, status int, description string) { func (s *Server) renderError(r *http.Request, w http.ResponseWriter, status int, description string) {
if err := s.templates.err(w, status, description); err != nil { if err := s.templates.err(r, w, status, description); err != nil {
s.logger.Errorf("Server template error: %v", err) s.logger.Errorf("Server template error: %v", err)
} }
} }

View file

@ -6,7 +6,9 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url"
"os" "os"
"path"
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
@ -94,7 +96,7 @@ func loadWebConfig(c webConfig) (static, theme http.Handler, templates *template
c.dir = "./web" c.dir = "./web"
} }
if c.logoURL == "" { if c.logoURL == "" {
c.logoURL = join(c.issuerURL, "theme/logo.png") c.logoURL = "theme/logo.png"
} }
if err := dirExists(c.dir); err != nil { if err := dirExists(c.dir); err != nil {
@ -136,10 +138,15 @@ func loadTemplates(c webConfig, templatesDir string) (*templates, error) {
return nil, fmt.Errorf("no files in template dir %q", templatesDir) return nil, fmt.Errorf("no files in template dir %q", templatesDir)
} }
issuerURL, err := url.Parse(c.issuerURL)
if err != nil {
return nil, fmt.Errorf("error parsing issuerURL: %v", err)
}
funcs := map[string]interface{}{ funcs := map[string]interface{}{
"issuer": func() string { return c.issuer }, "issuer": func() string { return c.issuer },
"logo": func() string { return c.logoURL }, "logo": func() string { return c.logoURL },
"url": func(s string) string { return join(c.issuerURL, s) }, "url": func(reqPath, assetPath string) string { return relativeURL(issuerURL.Path, reqPath, assetPath) },
"lower": strings.ToLower, "lower": strings.ToLower,
"extra": func(k string) string { return c.extra[k] }, "extra": func(k string) string { return c.extra[k] },
} }
@ -166,6 +173,69 @@ func loadTemplates(c webConfig, templatesDir string) (*templates, error) {
}, nil }, nil
} }
// relativeURL returns the URL of the asset relative to the URL of the request path.
// The serverPath is consulted to trim any prefix due in case it is not listening
// to the root path.
//
// Algorithm:
// 1. Remove common prefix of serverPath and reqPath
// 2. Remove common prefix of assetPath and reqPath
// 3. For each part of reqPath remaining(minus one), go up one level (..)
// 4. For each part of assetPath remaining, append it to result
//
//eg
//server listens at localhost/dex so serverPath is dex
//reqPath is /dex/auth
//assetPath is static/main.css
//relativeURL("/dex", "/dex/auth", "static/main.css") = "../static/main.css"
func relativeURL(serverPath, reqPath, assetPath string) string {
splitPath := func(p string) []string {
res := []string{}
parts := strings.Split(path.Clean(p), "/")
for _, part := range parts {
if part != "" {
res = append(res, part)
}
}
return res
}
stripCommonParts := func(s1, s2 []string) ([]string, []string) {
min := len(s1)
if len(s2) < min {
min = len(s2)
}
splitIndex := min
for i := 0; i < min; i++ {
if s1[i] != s2[i] {
splitIndex = i
break
}
}
return s1[splitIndex:], s2[splitIndex:]
}
server, req, asset := splitPath(serverPath), splitPath(reqPath), splitPath(assetPath)
// Remove common prefix of request path with server path
server, req = stripCommonParts(server, req)
// Remove common prefix of request path with asset path
asset, req = stripCommonParts(asset, req)
// For each part of the request remaining (minus one) -> go up one level (..)
// For each part of the asset remaining -> append it
var relativeURL string
for i := 0; i < len(req)-1; i++ {
relativeURL = path.Join("..", relativeURL)
}
relativeURL = path.Join(relativeURL, path.Join(asset...))
return relativeURL
}
var scopeDescriptions = map[string]string{ var scopeDescriptions = map[string]string{
"offline_access": "Have offline access", "offline_access": "Have offline access",
"profile": "View basic profile information", "profile": "View basic profile information",
@ -184,26 +254,28 @@ 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) 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 (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
func (t *templates) login(w http.ResponseWriter, connectors []connectorInfo) error { func (t *templates) login(r *http.Request, w http.ResponseWriter, connectors []connectorInfo, reqPath string) error {
sort.Sort(byName(connectors)) sort.Sort(byName(connectors))
data := struct { data := struct {
Connectors []connectorInfo Connectors []connectorInfo
}{connectors} ReqPath string
}{connectors, r.URL.Path}
return renderTemplate(w, t.loginTmpl, data) return renderTemplate(w, t.loginTmpl, data)
} }
func (t *templates) password(w http.ResponseWriter, postURL, lastUsername, usernamePrompt string, lastWasInvalid, showBacklink bool) error { func (t *templates) password(r *http.Request, w http.ResponseWriter, postURL, lastUsername, usernamePrompt string, lastWasInvalid, showBacklink bool, reqPath string) error {
data := struct { data := struct {
PostURL string PostURL string
BackLink bool BackLink bool
Username string Username string
UsernamePrompt string UsernamePrompt string
Invalid bool Invalid bool
}{postURL, showBacklink, lastUsername, usernamePrompt, lastWasInvalid} ReqPath string
}{postURL, showBacklink, lastUsername, usernamePrompt, lastWasInvalid, r.URL.Path}
return renderTemplate(w, t.passwordTmpl, data) return renderTemplate(w, t.passwordTmpl, data)
} }
func (t *templates) approval(w http.ResponseWriter, authReqID, username, clientName string, scopes []string) error { func (t *templates) approval(r *http.Request, w http.ResponseWriter, authReqID, username, clientName string, scopes []string, reqPath string) error {
accesses := []string{} accesses := []string{}
for _, scope := range scopes { for _, scope := range scopes {
access, ok := scopeDescriptions[scope] access, ok := scopeDescriptions[scope]
@ -217,23 +289,26 @@ func (t *templates) approval(w http.ResponseWriter, authReqID, username, clientN
Client string Client string
AuthReqID string AuthReqID string
Scopes []string Scopes []string
}{username, clientName, authReqID, accesses} ReqPath string
}{username, clientName, authReqID, accesses, r.URL.Path}
return renderTemplate(w, t.approvalTmpl, data) return renderTemplate(w, t.approvalTmpl, data)
} }
func (t *templates) oob(w http.ResponseWriter, code string) error { func (t *templates) oob(r *http.Request, w http.ResponseWriter, code string, reqPath string) error {
data := struct { data := struct {
Code string Code string
}{code} ReqPath string
}{code, r.URL.Path}
return renderTemplate(w, t.oobTmpl, data) return renderTemplate(w, t.oobTmpl, data)
} }
func (t *templates) err(w http.ResponseWriter, errCode int, errMsg string) error { func (t *templates) err(r *http.Request, w http.ResponseWriter, errCode int, errMsg string) error {
w.WriteHeader(errCode) w.WriteHeader(errCode)
data := struct { data := struct {
ErrType string ErrType string
ErrMsg string ErrMsg string
}{http.StatusText(errCode), errMsg} ReqPath string
}{http.StatusText(errCode), errMsg, r.URL.Path}
if err := t.errorTmpl.Execute(w, data); err != nil { if err := t.errorTmpl.Execute(w, data); err != nil {
return fmt.Errorf("Error rendering template %s: %s", t.errorTmpl.Name(), err) return fmt.Errorf("Error rendering template %s: %s", t.errorTmpl.Name(), err)
} }

View file

@ -1 +1,44 @@
package server package server
import "testing"
func TestRelativeURL(t *testing.T) {
tests := []struct {
name string
serverPath string
reqPath string
assetPath string
expected string
}{
{
name: "server-root-req-one-level-asset-two-level",
serverPath: "/",
reqPath: "/auth",
assetPath: "/theme/main.css",
expected: "theme/main.css",
},
{
name: "server-one-level-req-one-level-asset-two-level",
serverPath: "/dex",
reqPath: "/dex/auth",
assetPath: "/theme/main.css",
expected: "theme/main.css",
},
{
name: "server-root-req-two-level-asset-three-level",
serverPath: "/dex",
reqPath: "/dex/auth/connector",
assetPath: "assets/css/main.css",
expected: "../assets/css/main.css",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := relativeURL(test.serverPath, test.reqPath, test.assetPath)
if actual != test.expected {
t.Fatalf("Got '%s'. Expected '%s'", actual, test.expected)
}
})
}
}

View file

@ -5,15 +5,15 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>{{ issuer }}</title> <title>{{ issuer }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{ url "static/main.css" }}" rel="stylesheet"> <link href="{{ url .ReqPath "static/main.css" }}" rel="stylesheet">
<link href="{{ url "theme/styles.css" }}" rel="stylesheet"> <link href="{{ url .ReqPath "theme/styles.css" }}" rel="stylesheet">
<link rel="icon" href="{{ url "theme/favicon.png" }}"> <link rel="icon" href="{{ url .ReqPath "theme/favicon.png" }}">
</head> </head>
<body class="theme-body"> <body class="theme-body">
<div class="theme-navbar"> <div class="theme-navbar">
<div class="theme-navbar__logo-wrap"> <div class="theme-navbar__logo-wrap">
<img class="theme-navbar__logo" src="{{ logo }}"> <img class="theme-navbar__logo" src="{{ url .ReqPath logo }}">
</div> </div>
</div> </div>