OIDC implementation
This commit is contained in:
parent
9826f52be4
commit
bb190ab70d
27
auth/db/schema/mysql/20180704080000.base.up.sql
Normal file
27
auth/db/schema/mysql/20180704080000.base.up.sql
Normal 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;
|
||||
@ -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...),
|
||||
}
|
||||
}
|
||||
|
||||
60
auth/repository/settings.go
Normal file
60
auth/repository/settings.go
Normal 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
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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"`
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user