snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
This commit is contained in:
90
server/auth/external/auth_handler.go
vendored
Normal file
90
server/auth/external/auth_handler.go
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/auth/saml"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
"github.com/crewjam/saml/samlsp"
|
||||
"github.com/markbates/goth/gothic"
|
||||
)
|
||||
|
||||
type (
|
||||
ExternalAuthHandler interface {
|
||||
BeginUserAuth(http.ResponseWriter, *http.Request)
|
||||
CompleteUserAuth(http.ResponseWriter, *http.Request) (u *types.ExternalAuthUser, err error)
|
||||
}
|
||||
|
||||
externalSamlAuthHandler struct {
|
||||
service *saml.SamlSPService
|
||||
}
|
||||
|
||||
externalDefaultAuthHandler struct{}
|
||||
)
|
||||
|
||||
func (eh *externalDefaultAuthHandler) BeginUserAuth(w http.ResponseWriter, r *http.Request) {
|
||||
gothic.BeginAuthHandler(w, r)
|
||||
}
|
||||
|
||||
func (eh *externalDefaultAuthHandler) CompleteUserAuth(w http.ResponseWriter, r *http.Request) (u *types.ExternalAuthUser, err error) {
|
||||
gu, err := gothic.CompleteUserAuth(w, r)
|
||||
u = &types.ExternalAuthUser{gu}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (eh *externalSamlAuthHandler) BeginUserAuth(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := eh.service.Handler().Session.GetSession(r)
|
||||
|
||||
if err == samlsp.ErrNoSession {
|
||||
eh.service.Handler().HandleStartAuthFlow(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (eh *externalSamlAuthHandler) CompleteUserAuth(w http.ResponseWriter, r *http.Request) (u *types.ExternalAuthUser, err error) {
|
||||
var (
|
||||
session samlsp.Session
|
||||
)
|
||||
|
||||
if session, err = eh.service.Handler().Session.GetSession(r); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if session != nil {
|
||||
sess := session.(samlsp.JWTSessionClaims)
|
||||
|
||||
if sess.StandardClaims.Valid() != nil {
|
||||
return nil, sess.StandardClaims.Valid()
|
||||
}
|
||||
|
||||
u = &types.ExternalAuthUser{}
|
||||
u.Provider = "saml"
|
||||
|
||||
// get identifier for use with Corteza (email)
|
||||
u.Email = eh.service.GuessIdentifier(sess.Attributes)
|
||||
|
||||
// try to get email from jwt claims
|
||||
if u.Email == "" {
|
||||
u.Email = sess.StandardClaims.Subject
|
||||
u.Name = sess.StandardClaims.Subject
|
||||
u.NickName = sess.StandardClaims.Subject
|
||||
} else {
|
||||
u.Name = sess.Attributes.Get(eh.service.IDPUserMeta.Name)
|
||||
u.NickName = sess.Attributes.Get(eh.service.IDPUserMeta.Handle)
|
||||
}
|
||||
|
||||
u.UserID = sess.Attributes.Get("SessionIndex")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func NewSamlExternalHandler(s *saml.SamlSPService) *externalSamlAuthHandler {
|
||||
return &externalSamlAuthHandler{
|
||||
service: s,
|
||||
}
|
||||
}
|
||||
|
||||
func NewDefaultExternalHandler() *externalDefaultAuthHandler {
|
||||
return &externalDefaultAuthHandler{}
|
||||
}
|
||||
15
server/auth/external/external.go
vendored
Normal file
15
server/auth/external/external.go
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/markbates/goth/gothic"
|
||||
)
|
||||
|
||||
const (
|
||||
OIDC_PROVIDER_PREFIX = "openid-connect." // must match const in "github.com/cortezaproject/corteza/server/system/types" app_settings.go
|
||||
|
||||
)
|
||||
|
||||
func Init(store sessions.Store) {
|
||||
gothic.Store = store
|
||||
}
|
||||
66
server/auth/external/external_test.go
vendored
Normal file
66
server/auth/external/external_test.go
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_parseExternalProviderUrl(t *testing.T) {
|
||||
mustParseURL := func(in string) *url.URL {
|
||||
r, err := url.Parse(in)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
type args struct {
|
||||
in string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantP *url.URL
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"happy-path",
|
||||
args{"https://foo.bar"},
|
||||
mustParseURL("https://foo.bar"),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"bad input",
|
||||
args{":\\"},
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"add schema",
|
||||
args{"cortezaproject.org"},
|
||||
mustParseURL("https://cortezaproject.org"),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"add schema and remove well-known",
|
||||
args{"cortezaproject.org/some-subdir/" + WellKnown},
|
||||
mustParseURL("https://cortezaproject.org/some-subdir/"),
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotP, err := parseExternalProviderUrl(tt.args.in)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseExternalProviderUrl() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(gotP, tt.wantP) {
|
||||
t.Errorf("parseExternalProviderUrl() gotP = %v, want %v", gotP, tt.wantP)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
89
server/auth/external/goth.go
vendored
Normal file
89
server/auth/external/goth.go
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/auth/external/nylas"
|
||||
"github.com/cortezaproject/corteza/server/auth/settings"
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/providers/facebook"
|
||||
"github.com/markbates/goth/providers/github"
|
||||
"github.com/markbates/goth/providers/google"
|
||||
"github.com/markbates/goth/providers/linkedin"
|
||||
"github.com/markbates/goth/providers/openidConnect"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// We're expecting that our users will be able to complete
|
||||
// external auth loop in 15 minutes.
|
||||
const (
|
||||
WellKnown = "/.well-known/openid-configuration"
|
||||
)
|
||||
|
||||
func SetupGothProviders(log *zap.Logger, redirectUrl string, ep ...settings.Provider) {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
// Purge all previously configured providers
|
||||
if l := len(goth.GetProviders()); l > 0 {
|
||||
log.Debug("removing existing providers", zap.Int("count", l))
|
||||
goth.ClearProviders()
|
||||
}
|
||||
|
||||
log.Debug("initializing enabled external authentication providers", zap.Int("count", len(ep)))
|
||||
|
||||
for _, pc := range ep {
|
||||
var provider goth.Provider
|
||||
|
||||
log := log.With(zap.String("provider", pc.Handle))
|
||||
|
||||
redirect := pc.RedirectUrl
|
||||
if redirect == "" {
|
||||
// If redirect URL is not explicitly set for this provider,
|
||||
// generate one from template string
|
||||
redirect = strings.Replace(redirectUrl, "{provider}", pc.Handle, 1)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(pc.Handle, OIDC_PROVIDER_PREFIX) {
|
||||
if pc.IssuerUrl == "" {
|
||||
log.Error("failed to discover OIDC provider, URL empty")
|
||||
continue
|
||||
}
|
||||
|
||||
wellKnown := strings.TrimSuffix(pc.IssuerUrl, "/") + WellKnown
|
||||
|
||||
var scope []string
|
||||
if len(pc.Scope) > 0 {
|
||||
scope = strings.Split(pc.Scope, " ")
|
||||
} else {
|
||||
scope = append(scope, "email")
|
||||
}
|
||||
|
||||
if provider, err = openidConnect.New(pc.Key, pc.Secret, redirect, wellKnown, scope...); err != nil {
|
||||
log.Error("failed to discover OIDC provider", zap.Error(err), zap.String("well-known", wellKnown))
|
||||
continue
|
||||
} else {
|
||||
provider.SetName(pc.Handle)
|
||||
}
|
||||
} else {
|
||||
switch pc.Handle {
|
||||
case "github":
|
||||
provider = github.New(pc.Key, pc.Secret, redirect, "user:email")
|
||||
case "facebook":
|
||||
provider = facebook.New(pc.Key, pc.Secret, redirect, "email")
|
||||
case "google":
|
||||
provider = google.New(pc.Key, pc.Secret, redirect, "email")
|
||||
case "linkedin":
|
||||
provider = linkedin.New(pc.Key, pc.Secret, redirect, "email")
|
||||
case "nylas":
|
||||
provider = nylas.New(pc.Key, pc.Secret, redirect, "email")
|
||||
}
|
||||
}
|
||||
|
||||
if provider != nil {
|
||||
log.Info("external authentication provider added", zap.String("key", pc.Key))
|
||||
goth.UseProviders(provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
13
server/auth/external/main_test.go
vendored
Normal file
13
server/auth/external/main_test.go
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/logger"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
logger.SetDefault(logger.MakeDebugLogger())
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
172
server/auth/external/nylas/nylas.go
vendored
Normal file
172
server/auth/external/nylas/nylas.go
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
package nylas
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// These vars define the Authentication, Token, and API URLS for Nylas.
|
||||
var (
|
||||
AuthURL = "https://api.nylas.com/oauth/authorize"
|
||||
TokenURL = "https://api.nylas.com/oauth/token"
|
||||
ProfileURL = "https://api.Nylas.com/account"
|
||||
)
|
||||
|
||||
// New creates a new Nylas provider, and sets up important connection details.
|
||||
// You should always call `Nylas.New` to get a new Provider. Never try to create
|
||||
// one manually.
|
||||
func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
|
||||
return NewCustomisedURL(clientKey, secret, callbackURL, AuthURL, TokenURL, ProfileURL, scopes...)
|
||||
}
|
||||
|
||||
// NewCustomisedURL is similar to New(...) but can be used to set custom URLs to connect to
|
||||
func NewCustomisedURL(clientKey, secret, callbackURL, authURL, tokenURL, profileURL string, scopes ...string) *Provider {
|
||||
p := &Provider{
|
||||
ClientKey: clientKey,
|
||||
Secret: secret,
|
||||
CallbackURL: callbackURL,
|
||||
providerName: "nylas",
|
||||
profileURL: profileURL,
|
||||
}
|
||||
p.config = newConfig(p, authURL, tokenURL, scopes)
|
||||
return p
|
||||
}
|
||||
|
||||
// Provider is the implementation of `goth.Provider` for accessing Nylas.
|
||||
type Provider struct {
|
||||
ClientKey string
|
||||
Secret string
|
||||
CallbackURL string
|
||||
HTTPClient *http.Client
|
||||
config *oauth2.Config
|
||||
providerName string
|
||||
profileURL string
|
||||
}
|
||||
|
||||
// Name is the name used to retrieve this provider later.
|
||||
func (p *Provider) Name() string {
|
||||
return p.providerName
|
||||
}
|
||||
|
||||
// SetName is to update the name of the provider (needed in case of multiple providers of 1 type)
|
||||
func (p *Provider) SetName(name string) {
|
||||
p.providerName = name
|
||||
}
|
||||
|
||||
func (p *Provider) Client() *http.Client {
|
||||
return goth.HTTPClientWithFallBack(p.HTTPClient)
|
||||
}
|
||||
|
||||
// Debug is a no-op for the Nylas package.
|
||||
func (p *Provider) Debug(debug bool) {}
|
||||
|
||||
// BeginAuth asks Nylas for an authentication end-point.
|
||||
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
|
||||
url := p.config.AuthCodeURL(state)
|
||||
session := &Session{
|
||||
AuthURL: url,
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// FetchUser will go to Nylas and access basic information about the user.
|
||||
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
|
||||
sess := session.(*Session)
|
||||
user := goth.User{
|
||||
AccessToken: sess.AccessToken,
|
||||
Provider: p.Name(),
|
||||
}
|
||||
|
||||
if user.AccessToken == "" {
|
||||
// data is not yet retrieved since accessToken is still empty
|
||||
return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", p.profileURL, nil)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", "Bearer "+sess.AccessToken)
|
||||
response, err := p.Client().Do(req)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return user, fmt.Errorf("Nylas API responded with a %d trying to fetch user information", response.StatusCode)
|
||||
}
|
||||
|
||||
bits, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
err = json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
err = userFromReader(bytes.NewReader(bits), &user)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
return user, err
|
||||
}
|
||||
|
||||
func userFromReader(reader io.Reader, user *goth.User) error {
|
||||
u := struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email_address"`
|
||||
Name string `json:"name"`
|
||||
}{}
|
||||
|
||||
err := json.NewDecoder(reader).Decode(&u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.Name = u.Name
|
||||
user.NickName = u.Name
|
||||
user.Email = u.Email
|
||||
user.UserID = u.ID
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func newConfig(provider *Provider, authURL, tokenURL string, scopes []string) *oauth2.Config {
|
||||
c := &oauth2.Config{
|
||||
ClientID: provider.ClientKey,
|
||||
ClientSecret: provider.Secret,
|
||||
RedirectURL: provider.CallbackURL,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: authURL,
|
||||
TokenURL: tokenURL,
|
||||
},
|
||||
Scopes: []string{},
|
||||
}
|
||||
|
||||
c.Scopes = append(c.Scopes, scopes...)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
//RefreshToken refresh token is not provided by Nylas
|
||||
func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
|
||||
return nil, errors.New("Refresh token is not provided by Nylas")
|
||||
}
|
||||
|
||||
//RefreshTokenAvailable refresh token is not provided by Nylas
|
||||
func (p *Provider) RefreshTokenAvailable() bool {
|
||||
return false
|
||||
}
|
||||
56
server/auth/external/nylas/session.go
vendored
Normal file
56
server/auth/external/nylas/session.go
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
package nylas
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
)
|
||||
|
||||
// Session stores data during the auth process with Nylas.
|
||||
type Session struct {
|
||||
AuthURL string
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Nylas provider.
|
||||
func (s Session) GetAuthURL() (string, error) {
|
||||
if s.AuthURL == "" {
|
||||
return "", errors.New(goth.NoAuthUrlErrorMessage)
|
||||
}
|
||||
return s.AuthURL, nil
|
||||
}
|
||||
|
||||
// Authorize the session with Nylas and return the access token to be stored for future use.
|
||||
func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) {
|
||||
p := provider.(*Provider)
|
||||
token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !token.Valid() {
|
||||
return "", errors.New("Invalid token received from provider")
|
||||
}
|
||||
|
||||
s.AccessToken = token.AccessToken
|
||||
return token.AccessToken, err
|
||||
}
|
||||
|
||||
// Marshal the session into a string
|
||||
func (s Session) Marshal() string {
|
||||
b, _ := json.Marshal(s)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (s Session) String() string {
|
||||
return s.Marshal()
|
||||
}
|
||||
|
||||
// UnmarshalSession will unmarshal a JSON string into a session.
|
||||
func (p *Provider) UnmarshalSession(data string) (goth.Session, error) {
|
||||
sess := &Session{}
|
||||
err := json.NewDecoder(strings.NewReader(data)).Decode(sess)
|
||||
return sess, err
|
||||
}
|
||||
253
server/auth/external/register.go
vendored
Normal file
253
server/auth/external/register.go
vendored
Normal file
@@ -0,0 +1,253 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
"github.com/crusttech/go-oidc"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func AddProvider(ctx context.Context, log *zap.Logger, s store.SettingValues, eap *types.ExternalAuthProvider, force bool) error {
|
||||
var (
|
||||
prefix = "auth.external.providers." + eap.Key + "."
|
||||
)
|
||||
|
||||
log = log.With(
|
||||
zap.Bool("force", force),
|
||||
zap.String("handle", eap.Handle),
|
||||
zap.String("key", eap.Key),
|
||||
)
|
||||
|
||||
if eap.IssuerUrl != "" {
|
||||
log = log.With(zap.String("issuer-url", eap.IssuerUrl))
|
||||
}
|
||||
|
||||
log.Info("adding external authentication provider")
|
||||
|
||||
ss, _, err := store.SearchSettingValues(ctx, s, types.SettingsFilter{
|
||||
Prefix: prefix,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ex := ss.KV().CutPrefix(prefix)
|
||||
|
||||
if !force {
|
||||
// check if exists before storing it
|
||||
if len(ex) > 0 && ex.String("key") == eap.Key && ex.String("secret") == eap.Secret {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if vv, err := eap.EncodeKV(); err != nil {
|
||||
return fmt.Errorf("could not encode auth provider values: %w", err)
|
||||
} else if err = store.UpsertSettingValue(ctx, s, vv...); err != nil {
|
||||
return fmt.Errorf("could not store auth provider values: %w", err)
|
||||
}
|
||||
|
||||
log.Info("external authentication provider added")
|
||||
return nil
|
||||
}
|
||||
|
||||
// @todo remove dependency on github.com/crusttech/go-oidc (and github.com/coreos/go-oidc)
|
||||
//
|
||||
// and move client registration to corteza codebase
|
||||
func DiscoverOidcProvider(ctx context.Context, log *zap.Logger, opt options.AuthOpt, name, url string) (eap *types.ExternalAuthProvider, err error) {
|
||||
var (
|
||||
provider *oidc.Provider
|
||||
client *oidc.Client
|
||||
redirectUrl = strings.Replace(opt.ExternalRedirectURL, "{provider}", OIDC_PROVIDER_PREFIX+name, 1)
|
||||
)
|
||||
|
||||
log = log.With(
|
||||
zap.String("name", name),
|
||||
zap.String("url", url),
|
||||
)
|
||||
|
||||
if provider, err = oidc.NewProvider(ctx, url); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
client, err = provider.RegisterClient(ctx, &oidc.ClientRegistration{
|
||||
Name: "Corteza",
|
||||
RedirectURIs: []string{redirectUrl},
|
||||
ResponseTypes: []string{"token id_token", "code"},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error("could not register oidc provider", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
eap = &types.ExternalAuthProvider{
|
||||
Handle: OIDC_PROVIDER_PREFIX + name,
|
||||
RedirectUrl: redirectUrl,
|
||||
Key: client.ID,
|
||||
Secret: client.Secret,
|
||||
IssuerUrl: url,
|
||||
}
|
||||
|
||||
log.Info("oidc provider registered", zap.String("key", client.ID))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func RegisterOidcProvider(ctx context.Context, log *zap.Logger, s store.SettingValues, opt options.AuthOpt, name, providerUrl string, force, validate, enable bool) (eap *types.ExternalAuthProvider, err error) {
|
||||
if !force {
|
||||
prefix := "auth.external.providers." + eap.Key + "."
|
||||
|
||||
vv, _, err := store.SearchSettingValues(ctx, s, types.SettingsFilter{
|
||||
Prefix: prefix,
|
||||
})
|
||||
|
||||
if err != nil || len(vv) > 0 {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if validate {
|
||||
// Do basic validation of external auth settings
|
||||
// will fail if secret or url are not set
|
||||
if err = staticValidateExternal(opt); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Do full rediredct-URL check
|
||||
if err = validateExternalRedirectURL(opt); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if opt.ExternalRedirectURL == "" {
|
||||
return nil, fmt.Errorf("refusing to register OIDC provider without redirect url")
|
||||
}
|
||||
|
||||
p, err := parseExternalProviderUrl(providerUrl)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
eap, err = DiscoverOidcProvider(ctx, log, opt, name, p.String())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vv, err := eap.EncodeKV()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if enable {
|
||||
err = vv.Walk(func(value *types.SettingValue) error {
|
||||
if strings.HasSuffix(value.Name, ".enabled") {
|
||||
return value.SetSetting(true)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
v := &types.SettingValue{Name: "auth.external.enabled"}
|
||||
err = v.SetSetting(true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
vv = append(vv, v)
|
||||
}
|
||||
|
||||
err = store.UpsertSettingValue(ctx, s, vv...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func parseExternalProviderUrl(in string) (p *url.URL, err error) {
|
||||
if i := strings.Index(in, "://"); i == -1 {
|
||||
// Add schema if missing
|
||||
in = "https://" + in
|
||||
}
|
||||
|
||||
if p, err = url.Parse(in); err != nil {
|
||||
// Try to parse it
|
||||
return
|
||||
} else if i := strings.Index(p.Path, WellKnown); i > -1 {
|
||||
// Cut off well-known-path
|
||||
p.Path = p.Path[:i]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// StaticValidateExternal
|
||||
//
|
||||
// Simple checks of external auth settings
|
||||
func staticValidateExternal(opt options.AuthOpt) error {
|
||||
if opt.ExternalRedirectURL == "" {
|
||||
return fmt.Errorf("redirect URL is empty")
|
||||
}
|
||||
|
||||
const (
|
||||
tpt = "test-provider-test"
|
||||
)
|
||||
p, err := url.Parse(strings.Replace(opt.ExternalRedirectURL, "{provider}", tpt, 1))
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid redirect URL: %w", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(p.Path, tpt+"/callback") {
|
||||
return fmt.Errorf("could find injected provider in the URL, make sure you use '%%s' as a placeholder: %w", err)
|
||||
}
|
||||
|
||||
if opt.ExternalCookieSecret == "" {
|
||||
return fmt.Errorf("AUTH_EXTERNAL_COOKIE_SECRET is empty")
|
||||
}
|
||||
|
||||
if opt.SessionCookieSecure && p.Scheme != "https" {
|
||||
return fmt.Errorf("session store is secure, redirect URL should have HTTPS")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateExternalRedirectURL
|
||||
//
|
||||
// Validates external redirect URL
|
||||
func validateExternalRedirectURL(opt options.AuthOpt) error {
|
||||
const tpt = "test-provider-test"
|
||||
const cb = "/callback"
|
||||
|
||||
// Replace placeholders & remove /callback
|
||||
var url = strings.Replace(opt.ExternalRedirectURL, "{provider}", tpt, 1)
|
||||
url = url[0 : len(url)-len(cb)]
|
||||
|
||||
rsp, err := http.DefaultClient.Get(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get response from redirect URL: %w", err)
|
||||
}
|
||||
|
||||
defer rsp.Body.Close()
|
||||
body, err := ioutil.ReadAll(rsp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read response body: %w", err)
|
||||
}
|
||||
|
||||
if strings.Contains(string(body), tpt) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("could not validate external auth redirection URL")
|
||||
}
|
||||
Reference in New Issue
Block a user