eb0c312734
Add emailCiam setting to OIDC Connector for supporting an ID token that does not contain the email claim.
205 lines
5.4 KiB
Go
205 lines
5.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"
|
|
defaultEmailClaim = "email"
|
|
)
|
|
|
|
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"`
|
|
EmailClaim string `json:"emailClaim"`
|
|
}
|
|
|
|
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
|
|
emailClaim string
|
|
}
|
|
|
|
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,
|
|
emailClaim: cfg.EmailClaim,
|
|
}
|
|
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) Handler(errorURL url.URL) http.Handler {
|
|
return 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
|
|
}
|
|
|
|
// Override the email claim by using the value of the specified claim.
|
|
// This is used for the provider (e.g., Azure AD) which returns
|
|
// the ID token that does not include a email claim.
|
|
if c.emailClaim != "" && c.emailClaim != defaultEmailClaim {
|
|
email, ok, err := claims.StringClaim(c.emailClaim)
|
|
if err != nil {
|
|
log.Errorf("Unable to get value of alternative email claim: %v", err)
|
|
q.Set("error", oauth2.ErrorUnsupportedResponseType)
|
|
q.Set("error_description", "unable to get value of alternative email claim")
|
|
redirectError(w, errorURL, q)
|
|
return
|
|
}
|
|
|
|
if !ok {
|
|
log.Errorf("Failed parsing alternative email claim from remote provider: %v", err)
|
|
q.Set("error", oauth2.ErrorUnsupportedResponseType)
|
|
q.Set("error_description", "failed parsing alternative email claim")
|
|
redirectError(w, errorURL, q)
|
|
return
|
|
}
|
|
|
|
claims.Add(defaultEmailClaim, email)
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|