3
0

OIDC implementation

This commit is contained in:
Denis Arh 2018-09-18 16:39:38 +02:00
parent 9826f52be4
commit bb190ab70d
9 changed files with 182 additions and 88 deletions

View File

@ -0,0 +1,27 @@
CREATE TABLE settings (
name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',
value TEXT COMMENT 'Setting value',
PRIMARY KEY (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Keeps all known users, home and external organisation
-- changes are stored in audit log
CREATE TABLE users (
id BIGINT UNSIGNED NOT NULL,
email TEXT NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL,
name TEXT NOT NULL,
handle TEXT NOT NULL,
meta JSON NOT NULL,
rel_organisation BIGINT UNSIGNED NOT NULL,
created_at DATETIME NOT NULL DEFAULT NOW(),
updated_at DATETIME NULL,
suspended_at DATETIME NULL,
deleted_at DATETIME NULL, -- user soft delete
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

View File

@ -10,7 +10,7 @@ type (
appFlags struct {
http *config.HTTP
db *config.Database
jwt *config.JWT
//jwt *config.JWT
oidc *config.OIDC
}
)
@ -27,9 +27,9 @@ func (c *appFlags) Validate() error {
if err := c.db.Validate(); err != nil {
return err
}
if err := c.jwt.Validate(); err != nil {
return err
}
//if err := c.jwt.Validate(); err != nil {
// return err
//}
if err := c.oidc.Validate(); err != nil {
return err
}
@ -46,7 +46,7 @@ func Flags(prefix ...string) {
flags = &appFlags{
new(config.HTTP).Init(prefix...),
new(config.Database).Init(prefix...),
new(config.JWT).Init(prefix...),
//new(config.JWT).Init(prefix...),
new(config.OIDC).Init(prefix...),
}
}

View File

@ -0,0 +1,60 @@
package repository
import (
"context"
"encoding/json"
"github.com/pkg/errors"
)
type (
settings struct {
*repository
}
Settings interface {
Repository
With(context.Context) Settings
Get(name string, value interface{}) (bool, error)
Set(name string, value interface{}) error
}
)
func NewSettings(ctx context.Context) Settings {
return (&settings{}).With(ctx)
}
func (r *settings) With(ctx context.Context) Settings {
return &settings{
repository: r.repository.With(ctx),
}
}
func (r *settings) Set(name string, value interface{}) error {
if jsonValue, err := json.Marshal(value); err != nil {
return errors.Wrap(err, "Error marshaling settings value")
} else {
return r.db().Replace("settings", struct {
Key string `db:"name"`
Val json.RawMessage `db:"value"`
}{name, jsonValue})
}
}
func (r *settings) Get(name string, value interface{}) (bool, error) {
sql := "SELECT value FROM settings WHERE name = ?"
var stored json.RawMessage
if err := r.db().Get(&stored, sql, name); err != nil {
return false, errors.Wrap(err, "Error reading settings from the database")
} else if stored == nil {
return false, nil
} else if err := json.Unmarshal(stored, value); err != nil {
return false, errors.Wrap(err, "Error unmarshaling settings value")
}
return true, nil
}

View File

@ -2,18 +2,24 @@ package rest
import (
"context"
"github.com/coreos/go-oidc"
"github.com/crusttech/crust/auth/service"
"github.com/crusttech/crust/auth/types"
"github.com/crusttech/crust/config"
"github.com/titpetric/factory/resputil"
"golang.org/x/oauth2"
"math/rand"
"net/http"
"strconv"
"time"
"github.com/crusttech/crust/auth/repository"
"github.com/crusttech/crust/auth/service"
"github.com/crusttech/crust/auth/types"
"github.com/crusttech/crust/internal/auth"
"github.com/crusttech/crust/internal/config"
"github.com/crusttech/go-oidc"
"github.com/pkg/errors"
"github.com/titpetric/factory/resputil"
"golang.org/x/oauth2"
)
const DB_SETTINGS_KEY_OIDC_CLIENT = "oidc-client"
type (
openIdConnect struct {
provider *oidc.Provider
@ -29,14 +35,17 @@ type (
}
jwtEncodeCookieSetter interface {
types.TokenEncoder
SetToCookie(w http.ResponseWriter, r *http.Request, identity types.Identifiable)
auth.TokenEncoder
SetToCookie(w http.ResponseWriter, r *http.Request, identity auth.Identifiable)
}
)
const openIdConnectStateCookie = "oidc-state"
func OpenIdConnect(cfg *config.OIDC, usvc service.UserService, jwt jwtEncodeCookieSetter) (c *openIdConnect, err error) {
// Sets-up OIDC connection (issuer discovery, client registration)
//
// Client registration is done when no cfg.ClientID is provided.
func OpenIdConnect(ctx context.Context, cfg *config.OIDC, usvc service.UserService, jwt jwtEncodeCookieSetter, settings repository.Settings) (c *openIdConnect, err error) {
c = &openIdConnect{
appURL: cfg.AppURL,
stateCookieExpiry: cfg.StateCookieExpiry,
@ -45,25 +54,52 @@ func OpenIdConnect(cfg *config.OIDC, usvc service.UserService, jwt jwtEncodeCook
}
// Allow 5 seconds for issuer discovery process
c.provider, err = oidc.NewProvider(context.Background(), cfg.Issuer)
c.provider, err = oidc.NewProvider(ctx, cfg.Issuer)
if err != nil {
return nil, err
}
// Configure an OpenID Connect aware OAuth2 client.
c.config = oauth2.Config{
ClientID: cfg.ClientID,
ClientSecret: cfg.ClientSecret,
RedirectURL: cfg.RedirectURL,
if len(cfg.ClientID) > 0 {
// System is configured with fixed OIDC client ID (probably through AUTH_OIDC_CLIENT_ID)
// Construct oauth2 config from provided configuration
c.config = oauth2.Config{
ClientID: cfg.ClientID,
ClientSecret: cfg.ClientSecret,
RedirectURL: cfg.RedirectURL,
// Discovery returns the OAuth2 endpoints.
Endpoint: c.provider.Endpoint(),
// Discovery returns the OAuth2 endpoints.
Endpoint: c.provider.Endpoint(),
}
} else {
client := &oidc.Client{}
// "openid" is a required scope for OpenID Connect flows.
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
if found, err := settings.Get(DB_SETTINGS_KEY_OIDC_CLIENT, client); err != nil {
return nil, errors.Wrap(err, "could not load oidc client settings from the database")
} else if !found {
// Perform dynamic client registration
client, err = c.provider.RegisterClient(ctx, &oidc.ClientRegistration{
Name: "Crust",
RedirectURIs: []string{cfg.RedirectURL},
ResponseTypes: []string{"token id_token", "code"},
})
if err := settings.Set(DB_SETTINGS_KEY_OIDC_CLIENT, client); err != nil {
return nil, errors.Wrap(err, "could not store oidc client settings from the database")
}
}
c.config = c.provider.OAuth2Config(client)
}
c.verifier = c.provider.Verifier(&oidc.Config{ClientID: cfg.ClientID})
c.config.Scopes = []string{
oidc.ScopeOpenID,
"email",
"profile",
"address",
"phone_number",
}
c.verifier = c.provider.Verifier(&oidc.Config{ClientID: c.config.ClientID})
return
}
@ -110,33 +146,26 @@ func (c *openIdConnect) HandleOAuth2Callback(w http.ResponseWriter, r *http.Requ
}
// Parse and verify ID Token payload.
idToken, err := c.verifier.Verify(ctx, rawIDToken)
_, err = c.verifier.Verify(ctx, rawIDToken)
if err != nil {
resputil.JSON(w, err)
return
}
// Extract custom claims
var claims struct {
Email string `json:"email"`
Verified bool `json:"email_verified"`
u, _ := c.provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token))
var user = &types.User{
Email: u.Email,
}
if err := idToken.Claims(&claims); err != nil {
resputil.JSON(w, err)
return
}
var user *types.User
if user, err = c.userService.FindOrCreate(claims.Email); err != nil {
if user, err = c.userService.FindOrCreate(user); err != nil {
resputil.JSON(w, err)
return
} else {
c.jwt.SetToCookie(w, r, user)
}
http.Redirect(w, r, c.appURL+"?jwt="+c.jwt.Encode(user), http.StatusSeeOther)
http.Redirect(w, r, c.appURL, http.StatusSeeOther)
}
func (c *openIdConnect) stateCheck(r *http.Request) bool {

View File

@ -1,23 +1,27 @@
package rest
import (
"context"
"log"
"net/http"
"github.com/crusttech/crust/auth/repository"
"github.com/go-chi/chi"
"github.com/titpetric/factory/resputil"
"github.com/crusttech/crust/auth/rest/handlers"
"github.com/crusttech/crust/auth/service"
"github.com/crusttech/crust/config"
"github.com/crusttech/crust/internal/config"
)
func MountRoutes(oidcConfig *config.OIDC, jwtAuth jwtEncodeCookieSetter) func(chi.Router) {
var userSvc = service.User()
oidc, err := OpenIdConnect(oidcConfig, userSvc, jwtAuth)
var ctx = context.Background()
oidc, err := OpenIdConnect(ctx, oidcConfig, userSvc, jwtAuth, repository.NewSettings(ctx))
if err != nil {
log.Errorf("Could not initialize OIDC:", err.Error())
log.Print("Could not initialize OIDC:", err.Error())
}
// Initialize handers & controllers.

View File

@ -26,7 +26,7 @@ type (
Create(input *types.User) (*types.User, error)
Update(mod *types.User) (*types.User, error)
FindOrCreate(email string) (*types.User, error)
FindOrCreate(*types.User) (*types.User, error)
ValidateCredentials(username, password string) (*types.User, error)
}
)
@ -69,16 +69,16 @@ func (svc *user) Find(filter *types.UserFilter) ([]*types.User, error) {
}
// Finds if user with a specific email exists and returns it otherwise it creates a fresh one
func (svc *user) FindOrCreate(email string) (user *types.User, err error) {
return user, svc.repository.DB().Transaction(func() error {
if user, err = svc.repository.FindUserByEmail(email); err != repository.ErrUserNotFound {
return err
} else if user, err = svc.repository.CreateUser(&types.User{Email: email}); err != nil {
return err
}
func (svc *user) FindOrCreate(user *types.User) (out *types.User, err error) {
//return out, svc.repository.DB().Transaction(func() error {
if out, err = svc.repository.FindUserByEmail(user.Email); err != repository.ErrUserNotFound {
return out, err
} else if out, err = svc.repository.CreateUser(user); err != nil {
return out, err
}
return nil
})
return out, nil
//})
}
func (svc *user) Create(input *types.User) (user *types.User, err error) {

View File

@ -11,6 +11,8 @@ type (
ID uint64 `json:"id" db:"id"`
Username string `json:"username" db:"username"`
Email string `json:"email" db:"email"`
Name string `json:"name" db:"name"`
Handle string `json:"handle" db:"handle"`
Meta json.RawMessage `json:"-" db:"meta"`
OrganisationID uint64 `json:"organisationId" db:"rel_organisation"`
Password []byte `json:"-" db:"password"`

View File

@ -28,18 +28,9 @@ func (c *OIDC) Validate() error {
if c.Issuer == "" {
return errors.New("OIDC Issuer not set for AUTH")
}
if c.ClientID == "" {
return errors.New("OIDC ClientID not set for AUTH")
}
if c.ClientSecret == "" {
return errors.New("OIDC ClientSecret not set for AUTH")
}
if c.RedirectURL == "" {
return errors.New("OIDC RedirectURL not set for AUTH")
}
if c.AppURL == "" {
return errors.New("OIDC AppURL not set for AUTH")
}
return nil
}

View File

@ -36,7 +36,7 @@ CREATE TABLE channels (
type ENUM ('private', 'public', 'group') NOT NULL DEFAULT 'public',
rel_organisation BIGINT UNSIGNED NOT NULL REFERENCES organisation(id),
rel_creator BIGINT UNSIGNED NOT NULL REFERENCES users(id),
rel_creator BIGINT UNSIGNED NOT NULL,
created_at DATETIME NOT NULL DEFAULT NOW(),
updated_at DATETIME NULL,
@ -48,29 +48,10 @@ CREATE TABLE channels (
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Keeps all known users, home and external organisation
-- changes are stored in audit log
CREATE TABLE users (
id BIGINT UNSIGNED NOT NULL,
email TEXT NOT NULL,
username TEXT NOT NULL,
password TEXT,
meta JSON NOT NULL,
rel_organisation BIGINT UNSIGNED NOT NULL REFERENCES organisation(id),
created_at DATETIME NOT NULL DEFAULT NOW(),
updated_at DATETIME NULL,
suspended_at DATETIME NULL,
deleted_at DATETIME NULL, -- user soft delete
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Keeps team memberships
CREATE TABLE team_members (
rel_team BIGINT UNSIGNED NOT NULL REFERENCES organisation(id),
rel_user BIGINT UNSIGNED NOT NULL REFERENCES users(id),
rel_user BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (rel_team, rel_user)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@ -78,7 +59,7 @@ CREATE TABLE team_members (
-- handles channel membership
CREATE TABLE channel_members (
rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),
rel_user BIGINT UNSIGNED NOT NULL REFERENCES users(id),
rel_user BIGINT UNSIGNED NOT NULL,
type ENUM ('owner', 'member') NOT NULL DEFAULT 'member',
@ -90,7 +71,7 @@ CREATE TABLE channel_members (
CREATE TABLE channel_views (
rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),
rel_user BIGINT UNSIGNED NOT NULL REFERENCES users(id),
rel_user BIGINT UNSIGNED NOT NULL,
-- timestamp of last view, should be enough to find out which messaghr
viewed_at DATETIME NOT NULL DEFAULT NOW(),
@ -104,7 +85,7 @@ CREATE TABLE channel_views (
CREATE TABLE channel_pins (
rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),
rel_message BIGINT UNSIGNED NOT NULL REFERENCES messages(id),
rel_user BIGINT UNSIGNED NOT NULL REFERENCES users(id),
rel_user BIGINT UNSIGNED NOT NULL,
created_at DATETIME NOT NULL DEFAULT NOW(),
@ -116,7 +97,7 @@ CREATE TABLE messages (
type TEXT,
message TEXT NOT NULL,
meta JSON,
rel_user BIGINT UNSIGNED NOT NULL REFERENCES users(id),
rel_user BIGINT UNSIGNED NOT NULL,
rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),
reply_to BIGINT UNSIGNED NULL REFERENCES messages(id),
@ -129,7 +110,7 @@ CREATE TABLE messages (
CREATE TABLE reactions (
id BIGINT UNSIGNED NOT NULL,
rel_user BIGINT UNSIGNED NOT NULL REFERENCES users(id),
rel_user BIGINT UNSIGNED NOT NULL,
rel_message BIGINT UNSIGNED NOT NULL REFERENCES messages(id),
rel_channel BIGINT UNSIGNED NOT NULL REFERENCES channels(id),
reaction TEXT NOT NULL,
@ -141,7 +122,7 @@ CREATE TABLE reactions (
CREATE TABLE attachments (
id BIGINT UNSIGNED NOT NULL,
rel_user BIGINT UNSIGNED NOT NULL REFERENCES users(id),
rel_user BIGINT UNSIGNED NOT NULL,
url VARCHAR(512),
preview_url VARCHAR(512),