forked from mystiq/dex
5d284e08ae
HTTP code 307 aka. StatusTemporaryRedirect is used throughout the project. However, the endpoints redirected to explicitly expects the client to make a GET request. If a HTTP client issues a POST request to a server and receives a HTTP 307 redirect, it forwards the POST request to the new URL. When using 302 the HTTP client will issue a GET request. Fixes #287
177 lines
4.4 KiB
Go
177 lines
4.4 KiB
Go
package connector
|
|
|
|
import (
|
|
"html/template"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
|
|
phttp "github.com/coreos/dex/pkg/http"
|
|
"github.com/coreos/dex/pkg/log"
|
|
"github.com/coreos/go-oidc/oauth2"
|
|
"github.com/coreos/go-oidc/oidc"
|
|
)
|
|
|
|
const (
|
|
OIDCConnectorType = "oidc"
|
|
httpPathCallback = "/callback"
|
|
)
|
|
|
|
func init() {
|
|
RegisterConnectorConfigType(OIDCConnectorType, func() ConnectorConfig { return &OIDCConnectorConfig{} })
|
|
}
|
|
|
|
type OIDCConnectorConfig struct {
|
|
ID string `json:"id"`
|
|
IssuerURL string `json:"issuerURL"`
|
|
ClientID string `json:"clientID"`
|
|
ClientSecret string `json:"clientSecret"`
|
|
TrustedEmailProvider bool `json:"trustedEmailProvider"`
|
|
}
|
|
|
|
func (cfg *OIDCConnectorConfig) ConnectorID() string {
|
|
return cfg.ID
|
|
}
|
|
|
|
func (cfg *OIDCConnectorConfig) ConnectorType() string {
|
|
return OIDCConnectorType
|
|
}
|
|
|
|
type OIDCConnector struct {
|
|
id string
|
|
issuerURL string
|
|
cbURL url.URL
|
|
loginFunc oidc.LoginFunc
|
|
client *oidc.Client
|
|
trustedEmailProvider bool
|
|
}
|
|
|
|
func (cfg *OIDCConnectorConfig) Connector(ns url.URL, lf oidc.LoginFunc, tpls *template.Template) (Connector, error) {
|
|
ns.Path = path.Join(ns.Path, httpPathCallback)
|
|
|
|
ccfg := oidc.ClientConfig{
|
|
RedirectURL: ns.String(),
|
|
Credentials: oidc.ClientCredentials{
|
|
ID: cfg.ClientID,
|
|
Secret: cfg.ClientSecret,
|
|
},
|
|
}
|
|
|
|
cl, err := oidc.NewClient(ccfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
idpc := &OIDCConnector{
|
|
id: cfg.ID,
|
|
issuerURL: cfg.IssuerURL,
|
|
cbURL: ns,
|
|
loginFunc: lf,
|
|
client: cl,
|
|
trustedEmailProvider: cfg.TrustedEmailProvider,
|
|
}
|
|
return idpc, nil
|
|
}
|
|
|
|
func (c *OIDCConnector) ID() string {
|
|
return c.id
|
|
}
|
|
|
|
func (c *OIDCConnector) Healthy() error {
|
|
return c.client.Healthy()
|
|
}
|
|
|
|
func (c *OIDCConnector) LoginURL(sessionKey, prompt string) (string, error) {
|
|
oac, err := c.client.OAuthClient()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return oac.AuthCodeURL(sessionKey, "", prompt), nil
|
|
}
|
|
|
|
func (c *OIDCConnector) Register(mux *http.ServeMux, errorURL url.URL) {
|
|
mux.Handle(c.cbURL.Path, c.handleCallbackFunc(c.loginFunc, errorURL))
|
|
}
|
|
|
|
func (c *OIDCConnector) Sync() chan struct{} {
|
|
return c.client.SyncProviderConfig(c.issuerURL)
|
|
}
|
|
|
|
func (c *OIDCConnector) TrustedEmailProvider() bool {
|
|
return c.trustedEmailProvider
|
|
}
|
|
|
|
func redirectError(w http.ResponseWriter, errorURL url.URL, q url.Values) {
|
|
redirectURL := phttp.MergeQuery(errorURL, q)
|
|
w.Header().Set("Location", redirectURL.String())
|
|
w.WriteHeader(http.StatusSeeOther)
|
|
}
|
|
|
|
func (c *OIDCConnector) handleCallbackFunc(lf oidc.LoginFunc, errorURL url.URL) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
|
|
e := q.Get("error")
|
|
if e != "" {
|
|
redirectError(w, errorURL, q)
|
|
return
|
|
}
|
|
|
|
code := q.Get("code")
|
|
if code == "" {
|
|
q.Set("error", oauth2.ErrorInvalidRequest)
|
|
q.Set("error_description", "code query param must be set")
|
|
redirectError(w, errorURL, q)
|
|
return
|
|
}
|
|
|
|
tok, err := c.client.ExchangeAuthCode(code)
|
|
if err != nil {
|
|
log.Errorf("Unable to verify auth code with issuer: %v", err)
|
|
q.Set("error", oauth2.ErrorUnsupportedResponseType)
|
|
q.Set("error_description", "unable to verify auth code with issuer")
|
|
redirectError(w, errorURL, q)
|
|
return
|
|
}
|
|
|
|
claims, err := tok.Claims()
|
|
if err != nil {
|
|
log.Errorf("Unable to construct claims: %v", err)
|
|
q.Set("error", oauth2.ErrorUnsupportedResponseType)
|
|
q.Set("error_description", "unable to construct claims")
|
|
redirectError(w, errorURL, q)
|
|
return
|
|
}
|
|
|
|
ident, err := oidc.IdentityFromClaims(claims)
|
|
if err != nil {
|
|
log.Errorf("Failed parsing claims from remote provider: %v", err)
|
|
q.Set("error", oauth2.ErrorUnsupportedResponseType)
|
|
q.Set("error_description", "unable to convert claims to identity")
|
|
redirectError(w, errorURL, q)
|
|
return
|
|
}
|
|
|
|
sessionKey := q.Get("state")
|
|
if sessionKey == "" {
|
|
q.Set("error", oauth2.ErrorInvalidRequest)
|
|
q.Set("error_description", "missing state query param")
|
|
redirectError(w, errorURL, q)
|
|
return
|
|
}
|
|
|
|
redirectURL, err := lf(*ident, sessionKey)
|
|
if err != nil {
|
|
log.Errorf("Unable to log in %#v: %v", *ident, err)
|
|
q.Set("error", oauth2.ErrorAccessDenied)
|
|
q.Set("error_description", "login failed")
|
|
redirectError(w, errorURL, q)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Location", redirectURL)
|
|
w.WriteHeader(http.StatusFound)
|
|
return
|
|
}
|
|
}
|