forked from mystiq/dex
b80dbc8975
When "urn:ietf:wg:oauth:2.0:oob" is used as a redirect URI, redirect to an internal dex page where the user is shown the code and instructed to paste it into their app.
670 lines
20 KiB
Go
670 lines
20 KiB
Go
package server
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/coreos/go-oidc/jose"
|
|
"github.com/coreos/go-oidc/key"
|
|
"github.com/coreos/go-oidc/oauth2"
|
|
"github.com/coreos/go-oidc/oidc"
|
|
"github.com/coreos/pkg/health"
|
|
"github.com/go-gorp/gorp"
|
|
"github.com/jonboulle/clockwork"
|
|
|
|
"github.com/coreos/dex/client"
|
|
clientmanager "github.com/coreos/dex/client/manager"
|
|
"github.com/coreos/dex/connector"
|
|
"github.com/coreos/dex/pkg/log"
|
|
"github.com/coreos/dex/refresh"
|
|
"github.com/coreos/dex/scope"
|
|
"github.com/coreos/dex/session"
|
|
sessionmanager "github.com/coreos/dex/session/manager"
|
|
"github.com/coreos/dex/user"
|
|
usersapi "github.com/coreos/dex/user/api"
|
|
useremail "github.com/coreos/dex/user/email"
|
|
usermanager "github.com/coreos/dex/user/manager"
|
|
)
|
|
|
|
const (
|
|
LoginPageTemplateName = "login.html"
|
|
RegisterTemplateName = "register.html"
|
|
VerifyEmailTemplateName = "verify-email.html"
|
|
SendResetPasswordEmailTemplateName = "send-reset-password.html"
|
|
ResetPasswordTemplateName = "reset-password.html"
|
|
OOBTemplateName = "oob-template.html"
|
|
APIVersion = "v1"
|
|
)
|
|
|
|
type OIDCServer interface {
|
|
Client(string) (client.Client, error)
|
|
NewSession(connectorID, clientID, clientState string, redirectURL url.URL, nonce string, register bool, scope []string) (string, error)
|
|
Login(oidc.Identity, string) (string, error)
|
|
|
|
// CodeToken exchanges a code for an ID token and a refresh token string on success.
|
|
CodeToken(creds oidc.ClientCredentials, sessionKey string) (*jose.JWT, string, error)
|
|
|
|
ClientCredsToken(creds oidc.ClientCredentials) (*jose.JWT, error)
|
|
|
|
// RefreshToken takes a previously generated refresh token and returns a new ID token
|
|
// if the token is valid.
|
|
RefreshToken(creds oidc.ClientCredentials, scopes scope.Scopes, token string) (*jose.JWT, error)
|
|
|
|
KillSession(string) error
|
|
|
|
CrossClientAuthAllowed(requestingClientID, authorizingClientID string) (bool, error)
|
|
}
|
|
|
|
type JWTVerifierFactory func(clientID string) oidc.JWTVerifier
|
|
|
|
type Server struct {
|
|
IssuerURL url.URL
|
|
|
|
Templates *template.Template
|
|
LoginTemplate *template.Template
|
|
RegisterTemplate *template.Template
|
|
VerifyEmailTemplate *template.Template
|
|
SendResetPasswordEmailTemplate *template.Template
|
|
ResetPasswordTemplate *template.Template
|
|
OOBTemplate *template.Template
|
|
|
|
HealthChecks []health.Checkable
|
|
Connectors []connector.Connector
|
|
|
|
ClientRepo client.ClientRepo
|
|
ConnectorConfigRepo connector.ConnectorConfigRepo
|
|
KeySetRepo key.PrivateKeySetRepo
|
|
RefreshTokenRepo refresh.RefreshTokenRepo
|
|
UserRepo user.UserRepo
|
|
PasswordInfoRepo user.PasswordInfoRepo
|
|
|
|
ClientManager *clientmanager.ClientManager
|
|
KeyManager key.PrivateKeyManager
|
|
SessionManager *sessionmanager.SessionManager
|
|
UserManager *usermanager.UserManager
|
|
|
|
UserEmailer *useremail.UserEmailer
|
|
|
|
EnableRegistration bool
|
|
EnableClientRegistration bool
|
|
RegisterOnFirstLogin bool
|
|
|
|
dbMap *gorp.DbMap
|
|
localConnectorID string
|
|
}
|
|
|
|
func (s *Server) Run() chan struct{} {
|
|
stop := make(chan struct{})
|
|
|
|
chans := []chan struct{}{
|
|
key.NewKeySetSyncer(s.KeySetRepo, s.KeyManager).Run(),
|
|
}
|
|
|
|
for _, idpc := range s.Connectors {
|
|
chans = append(chans, idpc.Sync())
|
|
}
|
|
|
|
go func() {
|
|
<-stop
|
|
for _, ch := range chans {
|
|
close(ch)
|
|
}
|
|
}()
|
|
|
|
return stop
|
|
}
|
|
|
|
func (s *Server) KillSession(sessionKey string) error {
|
|
sessionID, err := s.SessionManager.ExchangeKey(sessionKey)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = s.SessionManager.Kill(sessionID)
|
|
return err
|
|
}
|
|
|
|
func (s *Server) ProviderConfig() oidc.ProviderConfig {
|
|
authEndpoint := s.absURL(httpPathAuth)
|
|
tokenEndpoint := s.absURL(httpPathToken)
|
|
keysEndpoint := s.absURL(httpPathKeys)
|
|
cfg := oidc.ProviderConfig{
|
|
Issuer: &s.IssuerURL,
|
|
AuthEndpoint: &authEndpoint,
|
|
TokenEndpoint: &tokenEndpoint,
|
|
KeysEndpoint: &keysEndpoint,
|
|
|
|
GrantTypesSupported: []string{oauth2.GrantTypeAuthCode, oauth2.GrantTypeClientCreds},
|
|
ResponseTypesSupported: []string{"code"},
|
|
SubjectTypesSupported: []string{"public"},
|
|
IDTokenSigningAlgValues: []string{"RS256"},
|
|
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"},
|
|
}
|
|
|
|
if s.EnableClientRegistration {
|
|
regEndpoint := s.absURL(httpPathClientRegistration)
|
|
cfg.RegistrationEndpoint = ®Endpoint
|
|
}
|
|
|
|
return cfg
|
|
}
|
|
|
|
func (s *Server) absURL(paths ...string) url.URL {
|
|
url := s.IssuerURL
|
|
paths = append([]string{url.Path}, paths...)
|
|
url.Path = path.Join(paths...)
|
|
return url
|
|
}
|
|
|
|
func (s *Server) AddConnector(cfg connector.ConnectorConfig) error {
|
|
connectorID := cfg.ConnectorID()
|
|
ns := s.IssuerURL
|
|
ns.Path = path.Join(ns.Path, httpPathAuth, connectorID)
|
|
|
|
idpc, err := cfg.Connector(ns, s.Login, s.Templates)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.Connectors = append(s.Connectors, idpc)
|
|
|
|
sortable := sortableIDPCs(s.Connectors)
|
|
sort.Sort(sortable)
|
|
|
|
// We handle the LocalConnector specially because it needs access to the
|
|
// UserRepo and the PasswordInfoRepo; if it turns out that other connectors
|
|
// need access to these resources we'll figure out how to provide it in a
|
|
// cleaner manner.
|
|
localConn, ok := idpc.(*connector.LocalConnector)
|
|
if ok {
|
|
s.localConnectorID = connectorID
|
|
if s.UserRepo == nil {
|
|
return errors.New("UserRepo cannot be nil")
|
|
}
|
|
|
|
if s.PasswordInfoRepo == nil {
|
|
return errors.New("PasswordInfoRepo cannot be nil")
|
|
}
|
|
|
|
localConn.SetLocalIdentityProvider(&connector.LocalIdentityProvider{
|
|
UserRepo: s.UserRepo,
|
|
PasswordInfoRepo: s.PasswordInfoRepo,
|
|
})
|
|
}
|
|
|
|
log.Infof("Loaded IdP connector: id=%s type=%s", connectorID, cfg.ConnectorType())
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) HTTPHandler() http.Handler {
|
|
checks := make([]health.Checkable, len(s.HealthChecks))
|
|
copy(checks, s.HealthChecks)
|
|
for _, idpc := range s.Connectors {
|
|
idpc := idpc
|
|
checks = append(checks, idpc)
|
|
}
|
|
|
|
clock := clockwork.NewRealClock()
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc(httpPathDiscovery, handleDiscoveryFunc(s.ProviderConfig()))
|
|
mux.HandleFunc(httpPathAuth, handleAuthFunc(s, s.Connectors, s.LoginTemplate, s.EnableRegistration))
|
|
mux.HandleFunc(httpPathOOB, handleOOBFunc(s, s.OOBTemplate))
|
|
mux.HandleFunc(httpPathToken, handleTokenFunc(s))
|
|
mux.HandleFunc(httpPathKeys, handleKeysFunc(s.KeyManager, clock))
|
|
mux.Handle(httpPathHealth, makeHealthHandler(checks))
|
|
|
|
if s.EnableRegistration {
|
|
mux.HandleFunc(httpPathRegister, handleRegisterFunc(s, s.RegisterTemplate))
|
|
}
|
|
|
|
mux.HandleFunc(httpPathEmailVerify, handleEmailVerifyFunc(s.VerifyEmailTemplate,
|
|
s.IssuerURL, s.KeyManager.PublicKeys, s.UserManager))
|
|
|
|
mux.Handle(httpPathVerifyEmailResend, s.NewClientTokenAuthHandler(handleVerifyEmailResendFunc(s.IssuerURL,
|
|
s.KeyManager.PublicKeys,
|
|
s.UserEmailer,
|
|
s.UserRepo,
|
|
s.ClientManager)))
|
|
|
|
mux.Handle(httpPathSendResetPassword, &SendResetPasswordEmailHandler{
|
|
tpl: s.SendResetPasswordEmailTemplate,
|
|
emailer: s.UserEmailer,
|
|
sm: s.SessionManager,
|
|
cm: s.ClientManager,
|
|
})
|
|
|
|
mux.Handle(httpPathResetPassword, &ResetPasswordHandler{
|
|
tpl: s.ResetPasswordTemplate,
|
|
issuerURL: s.IssuerURL,
|
|
um: s.UserManager,
|
|
keysFunc: s.KeyManager.PublicKeys,
|
|
})
|
|
|
|
mux.Handle(httpPathAcceptInvitation, &InvitationHandler{
|
|
passwordResetURL: s.absURL(httpPathResetPassword),
|
|
issuerURL: s.IssuerURL,
|
|
um: s.UserManager,
|
|
keysFunc: s.KeyManager.PublicKeys,
|
|
signerFunc: s.KeyManager.Signer,
|
|
redirectValidityWindow: s.SessionManager.ValidityWindow,
|
|
})
|
|
|
|
if s.EnableClientRegistration {
|
|
mux.HandleFunc(httpPathClientRegistration, s.handleClientRegistration)
|
|
}
|
|
|
|
mux.HandleFunc(httpPathDebugVars, health.ExpvarHandler)
|
|
|
|
pcfg := s.ProviderConfig()
|
|
for _, idpc := range s.Connectors {
|
|
errorURL, err := url.Parse(fmt.Sprintf("%s?connector_id=%s", pcfg.AuthEndpoint, idpc.ID()))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
idpc.Register(mux, *errorURL)
|
|
}
|
|
|
|
apiBasePath := path.Join(httpPathAPI, APIVersion)
|
|
registerDiscoveryResource(apiBasePath, mux)
|
|
|
|
usersAPI := usersapi.NewUsersAPI(s.UserManager, s.ClientManager, s.RefreshTokenRepo, s.UserEmailer, s.localConnectorID)
|
|
handler := NewUserMgmtServer(usersAPI, s.JWTVerifierFactory(), s.UserManager, s.ClientManager).HTTPHandler()
|
|
|
|
mux.Handle(apiBasePath+"/", handler)
|
|
|
|
return http.Handler(mux)
|
|
}
|
|
|
|
// NewClientTokenAuthHandler returns the given handler wrapped in middleware which requires a Client Bearer token.
|
|
func (s *Server) NewClientTokenAuthHandler(handler http.Handler) http.Handler {
|
|
return &clientTokenMiddleware{
|
|
issuerURL: s.IssuerURL.String(),
|
|
ciManager: s.ClientManager,
|
|
keysFunc: s.KeyManager.PublicKeys,
|
|
next: handler,
|
|
}
|
|
}
|
|
|
|
func (s *Server) Client(clientID string) (client.Client, error) {
|
|
return s.ClientManager.Get(clientID)
|
|
}
|
|
|
|
func (s *Server) NewSession(ipdcID, clientID, clientState string, redirectURL url.URL, nonce string, register bool, scope []string) (string, error) {
|
|
sessionID, err := s.SessionManager.NewSession(ipdcID, clientID, clientState, redirectURL, nonce, register, scope)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
log.Infof("Session %s created: clientID=%s clientState=%s", sessionID, clientID, clientState)
|
|
return s.SessionManager.NewSessionKey(sessionID)
|
|
}
|
|
|
|
func (s *Server) Login(ident oidc.Identity, key string) (string, error) {
|
|
sessionID, err := s.SessionManager.ExchangeKey(key)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
ses, err := s.SessionManager.AttachRemoteIdentity(sessionID, ident)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
log.Infof("Session %s remote identity attached: clientID=%s identity=%#v", sessionID, ses.ClientID, ident)
|
|
|
|
if ses.Register {
|
|
code, err := s.SessionManager.NewSessionKey(sessionID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
ru := s.absURL(httpPathRegister)
|
|
q := ru.Query()
|
|
q.Set("code", code)
|
|
q.Set("state", ses.ClientState)
|
|
ru.RawQuery = q.Encode()
|
|
return ru.String(), nil
|
|
}
|
|
|
|
remoteIdentity := user.RemoteIdentity{ConnectorID: ses.ConnectorID, ID: ses.Identity.ID}
|
|
|
|
// Get the connector used to log the user in.
|
|
var conn connector.Connector
|
|
for _, c := range s.Connectors {
|
|
if c.ID() == ses.ConnectorID {
|
|
conn = c
|
|
break
|
|
}
|
|
}
|
|
if conn == nil {
|
|
return "", fmt.Errorf("session contained invalid connector ID (%s)", ses.ConnectorID)
|
|
}
|
|
|
|
usr, err := s.UserRepo.GetByRemoteIdentity(nil, remoteIdentity)
|
|
if err == user.ErrorNotFound {
|
|
if ses.Identity.Email == "" {
|
|
// User doesn't have an existing account. Ask them to register.
|
|
u := newLoginURLFromSession(s.IssuerURL, ses, true, []string{ses.ConnectorID}, "register-maybe")
|
|
return u.String(), nil
|
|
}
|
|
|
|
// Does the user have an existing account with a different connector?
|
|
if connID, err := getConnectorForUserByEmail(s.UserRepo, ses.Identity.Email); err == nil {
|
|
// Ask user to sign in through existing account.
|
|
u := newLoginURLFromSession(s.IssuerURL, ses, false, []string{connID}, "wrong-connector")
|
|
return u.String(), nil
|
|
}
|
|
|
|
// RegisterOnFirstLogin doesn't work for the local connector
|
|
tryToRegister := s.RegisterOnFirstLogin && (ses.ConnectorID != s.localConnectorID)
|
|
|
|
if !tryToRegister {
|
|
// User doesn't have an existing account. Ask them to register.
|
|
u := newLoginURLFromSession(s.IssuerURL, ses, true, []string{ses.ConnectorID}, "register-maybe")
|
|
return u.String(), nil
|
|
}
|
|
|
|
// First time logging in through a remote connector. Attempt to register.
|
|
emailVerified := conn.TrustedEmailProvider()
|
|
usrID, err := s.UserManager.RegisterWithRemoteIdentity(ses.Identity.Email, emailVerified, remoteIdentity)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to register user: %v", err)
|
|
}
|
|
usr, err = s.UserManager.Get(usrID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("getting created user: %v", err)
|
|
}
|
|
} else if err != nil {
|
|
return "", fmt.Errorf("getting user: %v", err)
|
|
}
|
|
|
|
if usr.Disabled {
|
|
log.Errorf("user %s disabled", ses.Identity.Email)
|
|
return "", user.ErrorNotFound
|
|
}
|
|
|
|
ses, err = s.SessionManager.AttachUser(sessionID, usr.ID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("attaching user to session: %v", err)
|
|
}
|
|
log.Infof("Session %s user identified: clientID=%s user=%#v", sessionID, ses.ClientID, usr)
|
|
|
|
code, err := s.SessionManager.NewSessionKey(sessionID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("creating new session key: %v", err)
|
|
}
|
|
|
|
ru := ses.RedirectURL
|
|
if ru.String() == client.OOBRedirectURI {
|
|
ru = s.absURL(httpPathOOB)
|
|
}
|
|
q := ru.Query()
|
|
q.Set("code", code)
|
|
q.Set("state", ses.ClientState)
|
|
ru.RawQuery = q.Encode()
|
|
|
|
return ru.String(), nil
|
|
}
|
|
|
|
func (s *Server) ClientCredsToken(creds oidc.ClientCredentials) (*jose.JWT, error) {
|
|
cli, err := s.Client(creds.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if cli.Public {
|
|
return nil, oauth2.NewError(oauth2.ErrorInvalidClient)
|
|
}
|
|
|
|
ok, err := s.ClientManager.Authenticate(creds)
|
|
if err != nil {
|
|
log.Errorf("Failed fetching client %s from manager: %v", creds.ID, err)
|
|
return nil, oauth2.NewError(oauth2.ErrorServerError)
|
|
}
|
|
if !ok {
|
|
return nil, oauth2.NewError(oauth2.ErrorInvalidClient)
|
|
}
|
|
|
|
signer, err := s.KeyManager.Signer()
|
|
if err != nil {
|
|
log.Errorf("Failed to generate ID token: %v", err)
|
|
return nil, oauth2.NewError(oauth2.ErrorServerError)
|
|
}
|
|
|
|
now := time.Now()
|
|
exp := now.Add(s.SessionManager.ValidityWindow)
|
|
claims := oidc.NewClaims(s.IssuerURL.String(), creds.ID, creds.ID, now, exp)
|
|
claims.Add("name", creds.ID)
|
|
|
|
jwt, err := jose.NewSignedJWT(claims, signer)
|
|
if err != nil {
|
|
log.Errorf("Failed to generate ID token: %v", err)
|
|
return nil, oauth2.NewError(oauth2.ErrorServerError)
|
|
}
|
|
|
|
log.Infof("Client token sent: clientID=%s", creds.ID)
|
|
|
|
return jwt, nil
|
|
}
|
|
|
|
func (s *Server) CodeToken(creds oidc.ClientCredentials, sessionKey string) (*jose.JWT, string, error) {
|
|
ok, err := s.ClientManager.Authenticate(creds)
|
|
if err != nil {
|
|
log.Errorf("Failed fetching client %s from repo: %v", creds.ID, err)
|
|
return nil, "", oauth2.NewError(oauth2.ErrorServerError)
|
|
}
|
|
if !ok {
|
|
log.Errorf("Failed to Authenticate client %s", creds.ID)
|
|
return nil, "", oauth2.NewError(oauth2.ErrorInvalidClient)
|
|
}
|
|
|
|
sessionID, err := s.SessionManager.ExchangeKey(sessionKey)
|
|
if err != nil {
|
|
return nil, "", oauth2.NewError(oauth2.ErrorInvalidGrant)
|
|
}
|
|
|
|
ses, err := s.SessionManager.Kill(sessionID)
|
|
if err != nil {
|
|
return nil, "", oauth2.NewError(oauth2.ErrorInvalidRequest)
|
|
}
|
|
|
|
if ses.ClientID != creds.ID {
|
|
return nil, "", oauth2.NewError(oauth2.ErrorInvalidGrant)
|
|
}
|
|
|
|
signer, err := s.KeyManager.Signer()
|
|
if err != nil {
|
|
log.Errorf("Failed to generate ID token: %v", err)
|
|
return nil, "", oauth2.NewError(oauth2.ErrorServerError)
|
|
}
|
|
|
|
user, err := s.UserRepo.Get(nil, ses.UserID)
|
|
if err != nil {
|
|
log.Errorf("Failed to fetch user %q from repo: %v: ", ses.UserID, err)
|
|
return nil, "", oauth2.NewError(oauth2.ErrorServerError)
|
|
}
|
|
|
|
claims := ses.Claims(s.IssuerURL.String())
|
|
user.AddToClaims(claims)
|
|
|
|
s.addClaimsFromScope(claims, ses.Scope, ses.ClientID)
|
|
|
|
jwt, err := jose.NewSignedJWT(claims, signer)
|
|
if err != nil {
|
|
log.Errorf("Failed to generate ID token: %v", err)
|
|
return nil, "", oauth2.NewError(oauth2.ErrorServerError)
|
|
}
|
|
|
|
// Generate refresh token when 'scope' contains 'offline_access'.
|
|
var refreshToken string
|
|
|
|
for _, scope := range ses.Scope {
|
|
if scope == "offline_access" {
|
|
log.Infof("Session %s requests offline access, will generate refresh token", sessionID)
|
|
|
|
refreshToken, err = s.RefreshTokenRepo.Create(ses.UserID, creds.ID, ses.Scope)
|
|
switch err {
|
|
case nil:
|
|
break
|
|
default:
|
|
log.Errorf("Failed to generate refresh token: %v", err)
|
|
return nil, "", oauth2.NewError(oauth2.ErrorServerError)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
log.Infof("Session %s token sent: clientID=%s", sessionID, creds.ID)
|
|
return jwt, refreshToken, nil
|
|
}
|
|
|
|
func (s *Server) RefreshToken(creds oidc.ClientCredentials, scopes scope.Scopes, token string) (*jose.JWT, error) {
|
|
ok, err := s.ClientManager.Authenticate(creds)
|
|
if err != nil {
|
|
log.Errorf("Failed fetching client %s from repo: %v", creds.ID, err)
|
|
return nil, oauth2.NewError(oauth2.ErrorServerError)
|
|
}
|
|
if !ok {
|
|
log.Errorf("Failed to Authenticate client %s", creds.ID)
|
|
return nil, oauth2.NewError(oauth2.ErrorInvalidClient)
|
|
}
|
|
|
|
userID, rtScopes, err := s.RefreshTokenRepo.Verify(creds.ID, token)
|
|
switch err {
|
|
case nil:
|
|
break
|
|
case refresh.ErrorInvalidToken:
|
|
return nil, oauth2.NewError(oauth2.ErrorInvalidRequest)
|
|
case refresh.ErrorInvalidClientID:
|
|
return nil, oauth2.NewError(oauth2.ErrorInvalidClient)
|
|
default:
|
|
return nil, oauth2.NewError(oauth2.ErrorServerError)
|
|
}
|
|
|
|
if len(scopes) == 0 {
|
|
scopes = rtScopes
|
|
} else {
|
|
if !rtScopes.Contains(scopes) {
|
|
return nil, oauth2.NewError(oauth2.ErrorInvalidRequest)
|
|
}
|
|
}
|
|
|
|
user, err := s.UserRepo.Get(nil, userID)
|
|
if err != nil {
|
|
// The error can be user.ErrorNotFound, but we are not deleting
|
|
// user at this moment, so this shouldn't happen.
|
|
log.Errorf("Failed to fetch user %q from repo: %v: ", userID, err)
|
|
return nil, oauth2.NewError(oauth2.ErrorServerError)
|
|
}
|
|
|
|
signer, err := s.KeyManager.Signer()
|
|
if err != nil {
|
|
log.Errorf("Failed to refresh ID token: %v", err)
|
|
return nil, oauth2.NewError(oauth2.ErrorServerError)
|
|
}
|
|
|
|
now := time.Now()
|
|
expireAt := now.Add(session.DefaultSessionValidityWindow)
|
|
|
|
claims := oidc.NewClaims(s.IssuerURL.String(), user.ID, creds.ID, now, expireAt)
|
|
user.AddToClaims(claims)
|
|
|
|
s.addClaimsFromScope(claims, scope.Scopes(scopes), creds.ID)
|
|
|
|
jwt, err := jose.NewSignedJWT(claims, signer)
|
|
if err != nil {
|
|
log.Errorf("Failed to generate ID token: %v", err)
|
|
return nil, oauth2.NewError(oauth2.ErrorServerError)
|
|
}
|
|
|
|
log.Infof("New token sent: clientID=%s", creds.ID)
|
|
|
|
return jwt, nil
|
|
}
|
|
|
|
func (s *Server) CrossClientAuthAllowed(requestingClientID, authorizingClientID string) (bool, error) {
|
|
alloweds, err := s.ClientRepo.GetTrustedPeers(nil, authorizingClientID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
for _, allowed := range alloweds {
|
|
if requestingClientID == allowed {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func (s *Server) JWTVerifierFactory() JWTVerifierFactory {
|
|
noop := func() error { return nil }
|
|
|
|
keyFunc := func() []key.PublicKey {
|
|
keys, err := s.KeyManager.PublicKeys()
|
|
if err != nil {
|
|
log.Errorf("error getting public keys from manager: %v", err)
|
|
return []key.PublicKey{}
|
|
}
|
|
return keys
|
|
}
|
|
return func(clientID string) oidc.JWTVerifier {
|
|
|
|
return oidc.NewJWTVerifier(s.IssuerURL.String(), clientID, noop, keyFunc)
|
|
}
|
|
}
|
|
|
|
// addClaimsFromScope adds claims that are based on the scopes that the client requested.
|
|
// Currently, these include cross-client claims (aud, azp).
|
|
func (s *Server) addClaimsFromScope(claims jose.Claims, scopes scope.Scopes, clientID string) error {
|
|
crossClientIDs := scopes.CrossClientIDs()
|
|
if len(crossClientIDs) > 0 {
|
|
var aud []string
|
|
for _, id := range crossClientIDs {
|
|
if clientID == id {
|
|
aud = append(aud, id)
|
|
continue
|
|
}
|
|
allowed, err := s.CrossClientAuthAllowed(clientID, id)
|
|
if err != nil {
|
|
log.Errorf("Failed to check cross client auth. reqClientID %v; authClient:ID %v; err: %v", clientID, id, err)
|
|
return oauth2.NewError(oauth2.ErrorServerError)
|
|
}
|
|
if !allowed {
|
|
err := oauth2.NewError(oauth2.ErrorInvalidRequest)
|
|
err.Description = fmt.Sprintf(
|
|
"%q is not authorized to perform cross-client requests for %q",
|
|
clientID, id)
|
|
return err
|
|
}
|
|
aud = append(aud, id)
|
|
}
|
|
if len(aud) == 1 {
|
|
claims.Add("aud", aud[0])
|
|
} else {
|
|
claims.Add("aud", aud)
|
|
}
|
|
claims.Add("azp", clientID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type sortableIDPCs []connector.Connector
|
|
|
|
func (s sortableIDPCs) Len() int {
|
|
return len([]connector.Connector(s))
|
|
}
|
|
|
|
func (s sortableIDPCs) Less(i, j int) bool {
|
|
idpcs := []connector.Connector(s)
|
|
return idpcs[i].ID() < idpcs[j].ID()
|
|
}
|
|
|
|
func (s sortableIDPCs) Swap(i, j int) {
|
|
idpcs := []connector.Connector(s)
|
|
idpcs[i], idpcs[j] = idpcs[j], idpcs[i]
|
|
}
|