3
0

Add support for Nylas (and other) integration authentication

This commit is contained in:
Tomaž Jerman
2022-12-05 23:14:41 +01:00
committed by Jože Fortun
parent 9bd739cc9c
commit d53fa26044
17 changed files with 593 additions and 42 deletions

View File

@@ -184,18 +184,19 @@ func New(ctx context.Context, log *zap.Logger, oa2m oauth2def.Manager, s store.S
}
svc.handlers = &handlers.AuthHandlers{
Locale: locale.Global(),
Log: log,
Templates: tpls,
SessionManager: sesManager,
OAuth2: oauth2Server,
AuthService: systemService.DefaultAuth,
UserService: systemService.DefaultUser,
ClientService: &clientService{s},
TokenService: &tokenService{s},
DefaultClient: defClient,
Opt: svc.opt,
Settings: svc.settings,
Locale: locale.Global(),
Log: log,
Templates: tpls,
SessionManager: sesManager,
OAuth2: oauth2Server,
AuthService: systemService.DefaultAuth,
CredentialsService: systemService.DefaultCredentials,
UserService: systemService.DefaultUser,
ClientService: &clientService{s},
TokenService: &tokenService{s},
DefaultClient: defClient,
Opt: svc.opt,
Settings: svc.settings,
}
external.Init(sesManager.Store())

View File

@@ -3,6 +3,7 @@ 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"
@@ -75,6 +76,8 @@ func SetupGothProviders(log *zap.Logger, redirectUrl string, ep ...settings.Prov
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")
}
}

174
server/auth/external/nylas/nylas.go vendored Normal file
View File

@@ -0,0 +1,174 @@
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{},
}
for _, scope := range scopes {
c.Scopes = append(c.Scopes, scope)
}
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
View 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
}

View File

@@ -50,7 +50,6 @@ func AddProvider(ctx context.Context, log *zap.Logger, s store.SettingValues, ea
}
if vv, err := eap.EncodeKV(); err != nil {
return fmt.Errorf("could not encode auth provider values: %w", err)
return err
} else if err = store.UpsertSettingValue(ctx, s, vv...); err != nil {
return fmt.Errorf("could not store auth provider values: %w", err)
}

View File

@@ -8,7 +8,9 @@ import (
"github.com/cortezaproject/corteza/server/auth/external"
"github.com/cortezaproject/corteza/server/auth/request"
"github.com/cortezaproject/corteza/server/auth/settings"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
@@ -49,14 +51,25 @@ func (h AuthHandlers) handleSuccessfulExternalAuth(w http.ResponseWriter, r *htt
h.Log.Info("login successful", zap.String("provider", cred.Provider))
// Try to login/sign-up external user
if user, err = h.AuthService.External(ctx, cred); err != nil {
api.Send(w, r, err)
// Get the provider config so we can correctly handle the provided values
p := h.getProviderConfig(cred.Provider, h.Settings.Providers)
if p == nil {
api.Send(w, r, fmt.Errorf("credentials for provider %s are not registered in the system", cred.Provider))
return
}
h.handle(func(req *request.AuthReq) error {
req.AuthUser = request.NewAuthUser(
// For later, the request's auth user (no big deal if there isn't one)
au := request.GetAuthUser(h.SessionManager.Get(r))
// Check if we're using it as an identity provider; if so, use it to authenticate
if p.HasUsage(types.ExternalProviderUsageIdentity) {
// Try to login/sign-up external user
if user, err = h.AuthService.External(ctx, cred); err != nil {
api.Send(w, r, err)
return
}
au = request.NewAuthUser(
h.Settings,
user,
@@ -64,17 +77,89 @@ func (h AuthHandlers) handleSuccessfulExternalAuth(w http.ResponseWriter, r *htt
false,
)
// auto-complete EmailOTP and TOTP when authenticating via external identity provider
req.AuthUser.CompleteEmailOTP()
req.AuthUser.CompleteTOTP()
// If that's that, cut the flow here
if !p.HasUsage(types.ExternalProviderUsageAPI) {
h.handle(func(req *request.AuthReq) error {
req.AuthUser = au
req.AuthUser.Save(req.Session)
// auto-complete EmailOTP and TOTP when authenticating via external identity provider
req.AuthUser.CompleteEmailOTP()
req.AuthUser.CompleteTOTP()
req.AuthUser.Save(req.Session)
handleSuccessfulAuth(req)
return nil
})(w, r)
return
}
}
// Check if we're using it for an API integration; if so, note the access tokens
if p.HasUsage(types.ExternalProviderUsageAPI) {
if au.User == nil {
api.Send(w, r, fmt.Errorf("could not add credentials for user: not authenticated"))
return
}
ctx = auth.SetIdentityToContext(ctx, au.User)
// Look for existing
cc, err := h.CredentialsService.List(ctx, au.User.ID)
if err != nil {
api.Send(w, r, fmt.Errorf("couldn't fetch user credentials: %w", err))
return
}
// Find the existing one
kind := fmt.Sprintf("access-%s", cred.Provider)
var current *types.Credential
for _, c := range cc {
if c.Kind == kind && c.OwnerID == au.User.ID {
current = c
break
}
}
// Update
if current != nil {
current.Credentials = cred.AccessToken
_, err = h.CredentialsService.Update(ctx, current)
if err != nil {
api.Send(w, r, fmt.Errorf("couldn't update user credentials: %w", err))
return
}
} else {
_, err = h.CredentialsService.Create(ctx, &types.Credential{
Label: fmt.Sprintf("access token %s %s", cred.Provider, au.User.Email),
Kind: kind,
OwnerID: au.User.ID,
Credentials: cred.AccessToken,
})
if err != nil {
api.Send(w, r, fmt.Errorf("couldn't create user credentials: %w", err))
return
}
}
}
h.handle(func(req *request.AuthReq) error {
req.AuthUser = au
handleSuccessfulAuth(req)
return nil
})(w, r)
}
func (h AuthHandlers) getProviderConfig(handle string, set []settings.Provider) *settings.Provider {
for _, s := range set {
if s.Handle == handle {
return &s
}
}
return nil
}
func (h AuthHandlers) handleFailedExternalAuth(w http.ResponseWriter, _ *http.Request, err error) {
if strings.Contains(err.Error(), "Error processing your OAuth request: Invalid oauth_verifier parameter") {
// Just take user through the same loop again

View File

@@ -52,6 +52,13 @@ type (
ValidateEmailOTP(ctx context.Context, code string) (err error)
}
credentialsService interface {
List(ctx context.Context, userID uint64) (cc types.CredentialSet, err error)
Create(ctx context.Context, c *types.Credential) (*types.Credential, error)
Update(ctx context.Context, c *types.Credential) (*types.Credential, error)
Delete(ctx context.Context, userID, credentialsID uint64) (err error)
}
userService interface {
FindByAny(ctx context.Context, identifier interface{}) (*types.User, error)
Update(context.Context, *types.User) (*types.User, error)
@@ -101,18 +108,19 @@ type (
AuthHandlers struct {
Log *zap.Logger
Locale localeService
Templates templateExecutor
OAuth2 oauth2Service
SessionManager *request.SessionManager
AuthService authService
UserService userService
ClientService clientService
TokenService tokenService
DefaultClient *types.AuthClient
Opt options.AuthOpt
Settings *settings.Settings
SamlSPService *saml.SamlSPService
Locale localeService
Templates templateExecutor
OAuth2 oauth2Service
SessionManager *request.SessionManager
AuthService authService
CredentialsService credentialsService
UserService userService
ClientService clientService
TokenService tokenService
DefaultClient *types.AuthClient
Opt options.AuthOpt
Settings *settings.Settings
SamlSPService *saml.SamlSPService
}
handlerFn func(req *request.AuthReq) error
@@ -344,6 +352,10 @@ func (h *AuthHandlers) enrichTmplData(req *request.AuthReq) interface{} {
if _, err := goth.GetProvider(p.Handle); err != nil {
continue
}
// Skipping the ones we don't use for identity
if !p.HasUsage(types.ExternalProviderUsageIdentity) {
continue
}
out := provider{
Label: p.Label,

View File

@@ -79,6 +79,7 @@ type (
RedirectUrl string
Secret string
Scope string
Usage []string
}
BackgroundUI struct {
@@ -86,3 +87,13 @@ type (
Styles string
}
)
func (p Provider) HasUsage(u string) bool {
for _, pu := range p.Usage {
if pu == u {
return true
}
}
return false
}