Refactor credentials, migrate to /auth/external/ (from /social)
This commit is contained in:
+1
-8
@@ -19,15 +19,8 @@ SYSTEM_DB_PROFILER=stdout
|
||||
METRICS=1
|
||||
METRICS_PASSWORD=metrics
|
||||
|
||||
AUTH_JWT_SECRET=123
|
||||
AUTH_JWT_SECRET=jwt-secret
|
||||
AUTH_JWT_DEBUG=1
|
||||
AUTH_OIDC_ISSUER=123
|
||||
AUTH_OIDC_REDIRECT_URL=213
|
||||
AUTH_OIDC_COOKIE_DOMAIN=""
|
||||
|
||||
RBAC_AUTH=tenant:password
|
||||
RBAC_TENANT=tenant
|
||||
RBAC_BASE_URL=https://crust.example.org/pdp/
|
||||
|
||||
# Make sure you run `make mailhog.up` and visit localhost:8025.
|
||||
SMTP_HOST=localhost:1025
|
||||
|
||||
Generated
+3
-1
@@ -253,7 +253,7 @@
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c6a991538ddd77daaad1380ec85e1d7c6a0ae2bc2732b059ddb95f9a49569d3d"
|
||||
digest = "1:eb5f06b8a308f49f2205386975ab854606f01784546996964ade50fc41156875"
|
||||
name = "github.com/markbates/goth"
|
||||
packages = [
|
||||
".",
|
||||
@@ -262,6 +262,7 @@
|
||||
"providers/github",
|
||||
"providers/gplus",
|
||||
"providers/linkedin",
|
||||
"providers/openidConnect",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "157987f620ff2fc5e1f6a1427a3685219fbf6ff4"
|
||||
@@ -554,6 +555,7 @@
|
||||
"github.com/markbates/goth/providers/github",
|
||||
"github.com/markbates/goth/providers/gplus",
|
||||
"github.com/markbates/goth/providers/linkedin",
|
||||
"github.com/markbates/goth/providers/openidConnect",
|
||||
"github.com/namsral/flag",
|
||||
"github.com/pkg/errors",
|
||||
"github.com/prometheus/client_golang/prometheus",
|
||||
|
||||
@@ -27,7 +27,6 @@ func TestPermissions(t *testing.T) {
|
||||
user := &systemTypes.User{
|
||||
Name: "John Crm Doe",
|
||||
Username: "johndoe",
|
||||
SatosaID: "12345",
|
||||
}
|
||||
err := user.GeneratePassword("johndoe")
|
||||
NoError(t, err, "expected no error generating password, got %+v", err)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# Authentication
|
||||
|
||||
Crust support a fixed set of standard OAuth 2 authentication providers
|
||||
(facebook, gplus, github and linkedin) and a arbitrary number of custom
|
||||
issuers (over OpenID Connect).
|
||||
|
||||
# Available settings
|
||||
|
||||
Settings for external providers are stored under keys
|
||||
`auth.external.providers.<provider>.<prop>` and
|
||||
`auth.external.providers.openid-connect.<provider>.<prop>`.
|
||||
|
||||
Prop is one of: `key`, `secret`, `enabled`. OIDC settings also have `issuer` prop.
|
||||
|
||||
Example settings (`system-cli settings list --prefix=auth.external`):
|
||||
|
||||
|
||||
```
|
||||
auth.external.callback-endpoint "http://system.api.local.crust.tech:3002/auth/external/%s/callback"
|
||||
auth.external.enabled true
|
||||
auth.external.providers.facebook.enabled true
|
||||
auth.external.providers.facebook.key "24226007270326"
|
||||
auth.external.providers.facebook.secret "7vtfXx213cfc125804a226afcae777fe47"
|
||||
auth.external.providers.gplus true
|
||||
auth.external.providers.gplus.enabled true
|
||||
auth.external.providers.gplus.key "10629818561-7a8vr0avs47dqic43h2lkrurhr.apps.googleusercontent.com"
|
||||
auth.external.providers.gplus.secret "bkHmIFdk2YvtfXx"
|
||||
auth.external.providers.github.enabled false
|
||||
auth.external.providers.github.key null
|
||||
auth.external.providers.github.secret null
|
||||
auth.external.providers.linkedin.enabled false
|
||||
auth.external.providers.linkedin.key null
|
||||
auth.external.providers.linkedin.secret null
|
||||
auth.external.providers.openid-connect.didmos2.enabled true
|
||||
auth.external.providers.openid-connect.didmos2.key "tXM2ouiovowzGabk"
|
||||
auth.external.providers.openid-connect.didmos2.issuer "https://satosa.didmos.latest.crust.tech"
|
||||
auth.external.providers.openid-connect.didmos2.secret "e1d68bfd7718468ba8fd36131f5176b1"
|
||||
auth.external.redirect-url "http://system.api.local.crust.tech:3002/auth/external/%s/callback"
|
||||
auth.external.session-store-secret "fCVFSRWjVEcoYuhXSf3f6zVWO1p38XEWz2yS8WH7wKDbvpxFrZq7zlEuiUTvk4QF"
|
||||
```
|
||||
|
||||
# Changing settings
|
||||
|
||||
Authentication settings can be changed in the administration (via the API) and with cli
|
||||
command (`system-cli settings set <key> <value>`). Please bare in mind that values passed
|
||||
to CLI tool must always be in raw JSON format.
|
||||
|
||||
Changing values requires system service restart.
|
||||
|
||||
On startup, you should see log entries similar to these:
|
||||
```
|
||||
initializing external authentication providers (3)
|
||||
external authentication provider "facebook" added
|
||||
external authentication provider "gplus" added
|
||||
external authentication provider "openid-connect.didmos2" added
|
||||
```
|
||||
|
||||
|
||||
# OIDC Auto discovery/configuration
|
||||
|
||||
Crust CLI comes with auto-discovery tool:
|
||||
```bash
|
||||
system-cli external-auth auto-discovery name url
|
||||
```
|
||||
|
||||
```bash
|
||||
system-cli external-auth auto-discovery didmos2 https://satosa.didmos.crust.example.tld
|
||||
```
|
||||
|
||||
This will autodiscover and autoconfigure new OIDC provider.
|
||||
If entry with this name already exists it will override it.
|
||||
|
||||
Please note that this provider is disabled by default.
|
||||
|
||||
To enable it, run:
|
||||
```bash
|
||||
system-cli settings key auth.external.providers.openid-connect.didmos2.enabled true
|
||||
```
|
||||
@@ -1,60 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/namsral/flag"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type (
|
||||
OIDC struct {
|
||||
Enabled bool
|
||||
Issuer string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
|
||||
RedirectURL string
|
||||
AppURL string
|
||||
|
||||
StateCookieDomain string
|
||||
StateCookieExpiry int64
|
||||
}
|
||||
)
|
||||
|
||||
var oidc *OIDC
|
||||
|
||||
func (c *OIDC) Validate() error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
if c.Enabled == false {
|
||||
return nil
|
||||
}
|
||||
if c.Issuer == "" {
|
||||
return errors.New("OIDC Issuer not set for AUTH")
|
||||
}
|
||||
if c.RedirectURL == "" {
|
||||
return errors.New("OIDC RedirectURL not set for AUTH")
|
||||
}
|
||||
if c.StateCookieDomain == "" {
|
||||
return errors.New("OIDC CookieDomain not set")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*OIDC) Init(prefix ...string) *OIDC {
|
||||
if oidc != nil {
|
||||
return oidc
|
||||
}
|
||||
|
||||
oidc = new(OIDC)
|
||||
flag.BoolVar(&oidc.Enabled, "auth-oidc-enabled", true, "OIDC enabled")
|
||||
flag.StringVar(&oidc.Issuer, "auth-oidc-issuer", "", "OIDC Issuer")
|
||||
flag.StringVar(&oidc.ClientID, "auth-oidc-client-id", "", "OIDC Client ID")
|
||||
flag.StringVar(&oidc.ClientSecret, "auth-oidc-client-secret", "", "OIDC Client Secret")
|
||||
flag.StringVar(&oidc.RedirectURL, "auth-oidc-redirect-url", "", "OIDC RedirectURL")
|
||||
flag.StringVar(&oidc.AppURL, "auth-oidc-app-url", "", "OIDC AppURL")
|
||||
flag.StringVar(&oidc.StateCookieDomain, "auth-oidc-cookie-domain", "", "JWT Cookie domain")
|
||||
flag.Int64Var(&oidc.StateCookieExpiry, "auth-oidc-state-cookie-expiry", 15, "OIDC State cookie expiry in minutes")
|
||||
return oidc
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/namsral/flag"
|
||||
)
|
||||
|
||||
type (
|
||||
Social struct {
|
||||
Enabled bool
|
||||
|
||||
FacebookKey string
|
||||
FacebookSecret string
|
||||
GPlusKey string
|
||||
GPlusSecret string
|
||||
GitHubKey string
|
||||
GitHubSecret string
|
||||
LinkedInKey string
|
||||
LinkedInSecret string
|
||||
|
||||
Url string
|
||||
|
||||
SessionStoreSecret string
|
||||
SessionStoreExpiry int // seconds!
|
||||
}
|
||||
)
|
||||
|
||||
var social *Social
|
||||
|
||||
func (c *Social) Validate() error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.Enabled == false {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.SessionStoreSecret == "" {
|
||||
return errors.New("Session store secret not set for SOCIAL")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*Social) Init(prefix ...string) *Social {
|
||||
if social != nil {
|
||||
return social
|
||||
}
|
||||
|
||||
b := func(name string, k, s *string) {
|
||||
flag.StringVar(k, "auth-social-"+strings.ToLower(name)+"-key", "", name+" key")
|
||||
flag.StringVar(s, "auth-social-"+strings.ToLower(name)+"-secret", "", name+" secret")
|
||||
|
||||
}
|
||||
|
||||
social = new(Social)
|
||||
flag.BoolVar(&social.Enabled, "auth-social-enabled", false, "SocialAuth enabled")
|
||||
|
||||
b("Facebook", &social.FacebookKey, &social.FacebookSecret)
|
||||
b("GPlus", &social.GPlusKey, &social.GPlusSecret)
|
||||
b("GitHub", &social.GitHubKey, &social.GitHubSecret)
|
||||
b("LinkedIn", &social.LinkedInKey, &social.LinkedInSecret)
|
||||
|
||||
flag.StringVar(&social.Url, "auth-social-url", "", "Base URL")
|
||||
flag.StringVar(&social.SessionStoreSecret, "auth-social-session-store-secret", "", "Session store secret")
|
||||
flag.IntVar(&social.SessionStoreExpiry, "auth-social-state-cookie-expiry", 60*15, "SocialAuth State cookie expiry in seconds")
|
||||
return social
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package rand
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// credits: https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang
|
||||
|
||||
const (
|
||||
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||
)
|
||||
|
||||
var randSrc = rand.NewSource(time.Now().UnixNano())
|
||||
var mu sync.Mutex
|
||||
|
||||
func Bytes(n int) []byte {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
b := make([]byte, n)
|
||||
|
||||
// A randSrc.Int63() generates 63 random bits, enough for letterIdxMax characters!
|
||||
for i, cache, remain := n-1, randSrc.Int63(), letterIdxMax; i >= 0; {
|
||||
if remain == 0 {
|
||||
cache, remain = randSrc.Int63(), letterIdxMax
|
||||
}
|
||||
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
|
||||
b[i] = letterBytes[idx]
|
||||
i--
|
||||
}
|
||||
cache >>= letterIdxBits
|
||||
remain--
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/crusttech/crust/internal/settings"
|
||||
"github.com/crusttech/crust/system/internal/auth/external"
|
||||
)
|
||||
|
||||
// Will perform OpenID connect auto-configuration
|
||||
func ExternalAuth(ctx context.Context, rootCmd *cobra.Command, settingsService settings.Service) {
|
||||
exit := func(err error) {
|
||||
if err != nil {
|
||||
rootCmd.Printf("Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
autoDiscover := &cobra.Command{
|
||||
Use: "auto-discovery [name] [url]",
|
||||
Short: "Auto discovers new OIDC client",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var name, url = args[0], args[1]
|
||||
|
||||
if eas, err := external.ExternalAuthSettings(settingsService); err != nil {
|
||||
exit(err)
|
||||
} else if eap, err := external.RegisterNewOpenIdClient(ctx, eas, name, url); err != nil {
|
||||
exit(err)
|
||||
} else if vv, err := eap.MakeValueSet("openid-connect." + name); err != nil {
|
||||
exit(err)
|
||||
} else if err := settingsService.BulkSet(vv); err != nil {
|
||||
exit(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
settingsCmd := &cobra.Command{
|
||||
Use: "external-auth",
|
||||
Short: "External authentication",
|
||||
}
|
||||
|
||||
settingsCmd.AddCommand(autoDiscover)
|
||||
|
||||
rootCmd.AddCommand(settingsCmd)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE `sys_user` DROP `password`;
|
||||
ALTER TABLE `sys_user` DROP `satosa_id`;
|
||||
ALTER TABLE `sys_credentials` ADD `last_used_at` DATETIME NULL;
|
||||
@@ -12,8 +12,6 @@ type (
|
||||
http *config.HTTP
|
||||
monitor *config.Monitor
|
||||
db *config.Database
|
||||
oidc *config.OIDC
|
||||
social *config.Social
|
||||
}
|
||||
)
|
||||
|
||||
@@ -35,12 +33,6 @@ func (c *appFlags) Validate() error {
|
||||
if err := c.db.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.oidc.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.social.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -56,7 +48,5 @@ func Flags(prefix ...string) {
|
||||
new(config.HTTP).Init(prefix...),
|
||||
new(config.Monitor).Init(prefix...),
|
||||
new(config.Database).Init(prefix...),
|
||||
new(config.OIDC).Init(prefix...),
|
||||
new(config.Social).Init(prefix...),
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/crusttech/crust/internal/settings"
|
||||
)
|
||||
|
||||
func Init(settingsService settings.Service) {
|
||||
if eas, err := ExternalAuthSettings(settingsService); err != nil {
|
||||
log.Printf("failed load external authentication settings: %v", err)
|
||||
} else {
|
||||
setupGoth(eas)
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/gothic"
|
||||
"github.com/markbates/goth/providers/facebook"
|
||||
"github.com/markbates/goth/providers/github"
|
||||
"github.com/markbates/goth/providers/gplus"
|
||||
"github.com/markbates/goth/providers/linkedin"
|
||||
"github.com/markbates/goth/providers/openidConnect"
|
||||
)
|
||||
|
||||
// We're expecting that our users will be able to complete
|
||||
// external auth loop in 15 minutes.
|
||||
const (
|
||||
gothMaxSessionStoreAge = 60 * 15 // In seconds.
|
||||
)
|
||||
|
||||
func setupGoth(eas *externalAuthSettings) {
|
||||
if eas == nil || !eas.enabled {
|
||||
log.Printf("external authentication disabled (%v)", eas)
|
||||
return
|
||||
}
|
||||
|
||||
store := sessions.NewCookieStore([]byte(eas.sessionStoreSecret))
|
||||
store.MaxAge(gothMaxSessionStoreAge)
|
||||
store.Options.Path = "/auth/external"
|
||||
store.Options.HttpOnly = true
|
||||
store.Options.Secure = eas.sessionStoreSecure
|
||||
gothic.Store = store
|
||||
|
||||
setupGothProviders(eas)
|
||||
|
||||
}
|
||||
|
||||
func setupGothProviders(eas *externalAuthSettings) {
|
||||
var (
|
||||
err error
|
||||
scopes = []string{"email"}
|
||||
)
|
||||
|
||||
// Purge all previously configured providers
|
||||
if l := len(goth.GetProviders()); l > 0 {
|
||||
log.Println("removing existing providers", l)
|
||||
goth.ClearProviders()
|
||||
}
|
||||
|
||||
var enabled uint
|
||||
for _, pc := range eas.providers {
|
||||
if pc.enabled {
|
||||
enabled++
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("initializing external authentication providers (%d)", enabled)
|
||||
|
||||
for name, pc := range eas.providers {
|
||||
var provider goth.Provider
|
||||
|
||||
if !pc.enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Index(name, "openid-connect.") == 0 {
|
||||
if pc.issuerUrl == "" {
|
||||
log.Printf("failed to discover %q (issuer URL is empty)", name)
|
||||
continue
|
||||
}
|
||||
|
||||
wellKnown := strings.TrimSuffix(pc.issuerUrl, "/") + "/.well-known/openid-configuration"
|
||||
|
||||
if provider, err = openidConnect.New(pc.key, pc.secret, pc.redirectUrl, wellKnown, scopes...); err != nil {
|
||||
log.Printf("failed to discover %q (auto discovery URL: %s) OIDC provider: %v", name, wellKnown, err)
|
||||
continue
|
||||
} else {
|
||||
provider.SetName(name)
|
||||
}
|
||||
} else {
|
||||
switch name {
|
||||
case "github":
|
||||
provider = github.New(pc.key, pc.secret, pc.redirectUrl, scopes...)
|
||||
case "facebook":
|
||||
provider = facebook.New(pc.key, pc.secret, pc.redirectUrl, scopes...)
|
||||
case "gplus":
|
||||
provider = gplus.New(pc.key, pc.secret, pc.redirectUrl, scopes...)
|
||||
case "linkedin":
|
||||
provider = linkedin.New(pc.key, pc.secret, pc.redirectUrl, scopes...)
|
||||
}
|
||||
}
|
||||
|
||||
if provider != nil {
|
||||
log.Printf("external authentication provider %q added", name)
|
||||
goth.UseProviders(provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/crusttech/go-oidc"
|
||||
)
|
||||
|
||||
func RegisterNewOpenIdClient(ctx context.Context, eas *externalAuthSettings, name, url string) (eap *externalAuthProvider, err error) {
|
||||
var (
|
||||
provider *oidc.Provider
|
||||
client *oidc.Client
|
||||
redirectUrl = fmt.Sprintf(eas.redirectUrl, "openid-connect."+name)
|
||||
)
|
||||
|
||||
if provider, err = oidc.NewProvider(ctx, url); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
client, err = provider.RegisterClient(ctx, &oidc.ClientRegistration{
|
||||
Name: "Crust",
|
||||
RedirectURIs: []string{redirectUrl},
|
||||
ResponseTypes: []string{"token id_token", "code"},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
eap = &externalAuthProvider{
|
||||
redirectUrl: redirectUrl,
|
||||
key: client.ID,
|
||||
secret: client.Secret,
|
||||
issuerUrl: url,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/crusttech/crust/internal/rand"
|
||||
intset "github.com/crusttech/crust/internal/settings"
|
||||
)
|
||||
|
||||
const (
|
||||
settingsKeyBase = "auth.external."
|
||||
|
||||
settingsKeyProviders = settingsKeyBase + "providers."
|
||||
settingsKeyRedirectUrl = settingsKeyBase + "redirect-url"
|
||||
settingsKeySessionStoreSecret = settingsKeyBase + "session-store-secret"
|
||||
settingsKeySessionStoreSecure = settingsKeyBase + "session-store-secure"
|
||||
)
|
||||
|
||||
type (
|
||||
externalAuthSettings struct {
|
||||
enabled bool
|
||||
redirectUrl string
|
||||
sessionStoreSecret string
|
||||
sessionStoreSecure bool
|
||||
providers map[string]externalAuthProvider
|
||||
|
||||
service intset.Service
|
||||
}
|
||||
|
||||
externalAuthProvider struct {
|
||||
enabled bool
|
||||
key string
|
||||
secret string
|
||||
redirectUrl string
|
||||
issuerUrl string
|
||||
}
|
||||
)
|
||||
|
||||
func ExternalAuthProvider(kv intset.KV) (eap externalAuthProvider, err error) {
|
||||
for k, v := range kv {
|
||||
ld := strings.LastIndex(k, ".")
|
||||
|
||||
switch k[ld+1:] {
|
||||
case "enabled":
|
||||
err = v.Unmarshal(&eap.enabled)
|
||||
case "key":
|
||||
err = v.Unmarshal(&eap.key)
|
||||
case "secret":
|
||||
err = v.Unmarshal(&eap.secret)
|
||||
case "issuer":
|
||||
err = v.Unmarshal(&eap.issuerUrl)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (eap externalAuthProvider) MakeValueSet(name string) (vv intset.ValueSet, err error) {
|
||||
set := func(name string, value interface{}) error {
|
||||
v := &intset.Value{Name: name}
|
||||
if v.Value, err = json.Marshal(value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vv = append(vv, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
prefix := settingsKeyProviders + name
|
||||
|
||||
if err = set(prefix+".enabled", eap.enabled); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = set(prefix+".key", eap.key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = set(prefix+".secret", eap.secret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = set(prefix+".issuer", eap.issuerUrl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vv, err
|
||||
}
|
||||
|
||||
func ExternalAuthSettings(s intset.Service) (eas *externalAuthSettings, err error) {
|
||||
// Read all settings and populate struct
|
||||
settings, err := s.FindByPrefix(settingsKeyBase)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not load settings for external auth provider")
|
||||
}
|
||||
|
||||
kv := settings.KV()
|
||||
|
||||
eas = &externalAuthSettings{
|
||||
enabled: kv.Bool(settingsKeyBase + "enabled"),
|
||||
redirectUrl: kv.String(settingsKeyRedirectUrl),
|
||||
sessionStoreSecret: kv.String(settingsKeySessionStoreSecret),
|
||||
}
|
||||
|
||||
storeGenerated := func(name string, value interface{}) (err error) {
|
||||
v := &intset.Value{Name: name}
|
||||
if v.Value, err = json.Marshal(value); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return s.Set(v)
|
||||
}
|
||||
|
||||
if eas.redirectUrl == "" {
|
||||
path := "/auth/external/%s/callback"
|
||||
if leHost, has := os.LookupEnv("LETSENCRYPT_HOST"); has {
|
||||
eas.redirectUrl = "https://" + leHost + path
|
||||
} else if vHost, has := os.LookupEnv("VIRTUAL_HOST"); has {
|
||||
eas.redirectUrl = "http://" + vHost + path
|
||||
} else {
|
||||
// Fallback to local
|
||||
eas.redirectUrl = "http://system.api.local.crust.tech" + path
|
||||
}
|
||||
|
||||
if err = storeGenerated(settingsKeyRedirectUrl, eas.redirectUrl); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if eas.sessionStoreSecret == "" {
|
||||
eas.sessionStoreSecret = string(rand.Bytes(64))
|
||||
if err = storeGenerated(settingsKeySessionStoreSecret, eas.sessionStoreSecret); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !kv.Has(settingsKeySessionStoreSecure) {
|
||||
// Assume we want to use secure store if we are accessed via HTTPS
|
||||
eas.sessionStoreSecure = strings.Index(eas.redirectUrl, "https://") == 0
|
||||
} else {
|
||||
eas.sessionStoreSecure = kv.Bool(settingsKeySessionStoreSecure)
|
||||
}
|
||||
|
||||
if eas.providers, err = extractProviders(eas.redirectUrl, kv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func extractProviders(redirectUrl string, kv intset.KV) (providers map[string]externalAuthProvider, err error) {
|
||||
// Standard providers:
|
||||
providers = map[string]externalAuthProvider{
|
||||
"github": {},
|
||||
"facebook": {},
|
||||
"gplus": {},
|
||||
"linkedin": {},
|
||||
}
|
||||
|
||||
oidcKeyBase := settingsKeyProviders + "openid-connect."
|
||||
for k := range kv.Filter(oidcKeyBase) {
|
||||
if len(k) < len(oidcKeyBase)+2 {
|
||||
// skip invalid keys
|
||||
continue
|
||||
}
|
||||
|
||||
// find next dot:
|
||||
name := k[len(oidcKeyBase):]
|
||||
dotPos := strings.Index(name, ".")
|
||||
if dotPos > 0 {
|
||||
name = name[:dotPos]
|
||||
}
|
||||
|
||||
providers["openid-connect."+name] = externalAuthProvider{}
|
||||
}
|
||||
|
||||
for provider := range providers {
|
||||
if eap, err := ExternalAuthProvider(kv.Filter(settingsKeyProviders + provider)); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
if eap.enabled {
|
||||
eap.redirectUrl = fmt.Sprintf(redirectUrl, provider)
|
||||
}
|
||||
|
||||
providers[provider] = eap
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
intset "github.com/crusttech/crust/internal/settings"
|
||||
"github.com/jmoiron/sqlx/types"
|
||||
)
|
||||
|
||||
func Test_extractProviders(t *testing.T) {
|
||||
type args struct {
|
||||
redirectUrl string
|
||||
kv intset.KV
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantProviders map[string]externalAuthProvider
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Empty KV",
|
||||
args: args{},
|
||||
wantProviders: map[string]externalAuthProvider{
|
||||
"github": externalAuthProvider{},
|
||||
"linkedin": externalAuthProvider{},
|
||||
"gplus": externalAuthProvider{},
|
||||
"facebook": externalAuthProvider{},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "Random config",
|
||||
args: args{
|
||||
redirectUrl: "http://%s",
|
||||
kv: intset.KV{
|
||||
"auth.external.providers.openid-connect.foo.enabled": types.JSONText("true"),
|
||||
"auth.external.providers.openid-connect.foo.issuer": types.JSONText(`"url"`),
|
||||
"auth.external.providers.openid-connect.foo.key": types.JSONText(`"key"`),
|
||||
"auth.external.providers.openid-connect.foo.secret": types.JSONText(`"secret"`),
|
||||
"auth.external.providers.openid-connect.bar.enabled": types.JSONText("true"),
|
||||
"auth.external.providers.openid-connect.bar.issuer": types.JSONText(`"url"`),
|
||||
"auth.external.providers.openid-connect.bar.key": types.JSONText(`"key"`),
|
||||
"auth.external.providers.openid-connect.bar.secret": types.JSONText(`"secret"`),
|
||||
"auth.external.providers.openid-connect.baz.enabled": types.JSONText("false"),
|
||||
"auth.external.providers.github.enabled": types.JSONText(`false`),
|
||||
"auth.external.providers.facebook.enabled": types.JSONText(`true`),
|
||||
"auth.external.providers.facebook.secret": types.JSONText(`"fb-secret"`),
|
||||
},
|
||||
},
|
||||
|
||||
wantProviders: map[string]externalAuthProvider{
|
||||
"openid-connect.foo": externalAuthProvider{
|
||||
enabled: true,
|
||||
key: "key",
|
||||
secret: "secret",
|
||||
redirectUrl: "http://openid-connect.foo",
|
||||
issuerUrl: "url",
|
||||
},
|
||||
"openid-connect.bar": externalAuthProvider{
|
||||
enabled: true,
|
||||
key: "key",
|
||||
secret: "secret",
|
||||
redirectUrl: "http://openid-connect.bar",
|
||||
issuerUrl: "url",
|
||||
},
|
||||
"openid-connect.baz": externalAuthProvider{
|
||||
enabled: false,
|
||||
},
|
||||
"github": externalAuthProvider{
|
||||
enabled: false,
|
||||
},
|
||||
"linkedin": externalAuthProvider{
|
||||
enabled: false,
|
||||
},
|
||||
"gplus": externalAuthProvider{
|
||||
enabled: false,
|
||||
},
|
||||
"facebook": externalAuthProvider{
|
||||
enabled: true,
|
||||
secret: "fb-secret",
|
||||
redirectUrl: "http://facebook",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotProviders, err := extractProviders(tt.args.redirectUrl, tt.args.kv)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("extractProviders() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(gotProviders, tt.wantProviders) {
|
||||
t.Errorf("extractProviders() = %v, want %v", gotProviders, tt.wantProviders)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalAuthProvider(t *testing.T) {
|
||||
type args struct {
|
||||
kv intset.KV
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantEap externalAuthProvider
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
args: args{
|
||||
kv: intset.KV{
|
||||
"foo.enabled": types.JSONText("true"),
|
||||
"foo.issuer": types.JSONText(`"example.tld"`),
|
||||
"foo.key": types.JSONText(`"key"`),
|
||||
"foo.secret": types.JSONText(`"secret"`),
|
||||
},
|
||||
},
|
||||
|
||||
wantEap: externalAuthProvider{
|
||||
enabled: true,
|
||||
key: "key",
|
||||
secret: "secret",
|
||||
issuerUrl: "example.tld",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotEap, err := ExternalAuthProvider(tt.args.kv)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ExternalAuthProvider() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(gotEap, tt.wantEap) {
|
||||
t.Errorf("ExternalAuthProvider() = %v, want %v", gotEap, tt.wantEap)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,13 @@ type (
|
||||
With(ctx context.Context, db *factory.DB) CredentialsRepository
|
||||
|
||||
FindByID(ID uint64) (*types.Credentials, error)
|
||||
FindByCredentials(kind types.CredentialsKind, credentials string) (cc types.CredentialsSet, err error)
|
||||
FindByKind(ownerID uint64, kind types.CredentialsKind) (cc types.CredentialsSet, err error)
|
||||
FindByCredentials(kind, credentials string) (cc types.CredentialsSet, err error)
|
||||
FindByKind(ownerID uint64, kind string) (cc types.CredentialsSet, err error)
|
||||
FindByOwnerID(ownerID uint64) (cc types.CredentialsSet, err error)
|
||||
Find() (cc types.CredentialsSet, err error)
|
||||
|
||||
Create(c *types.Credentials) (*types.Credentials, error)
|
||||
Update(c *types.Credentials) (*types.Credentials, error)
|
||||
DeleteByID(id uint64) error
|
||||
}
|
||||
|
||||
@@ -59,14 +60,14 @@ func (r *credentials) FindByID(ID uint64) (*types.Credentials, error) {
|
||||
return mod, isFound(r.db().Get(mod, sql, ID), mod.ID > 0, ErrCredentialsNotFound)
|
||||
}
|
||||
|
||||
func (r *credentials) FindByCredentials(kind types.CredentialsKind, credentials string) (cc types.CredentialsSet, err error) {
|
||||
func (r *credentials) FindByCredentials(kind, credentials string) (cc types.CredentialsSet, err error) {
|
||||
return r.fetchSet(
|
||||
fmt.Sprintf(sqlCredentialsSelect+" AND kind = ? AND credentials = ?", r.tblname),
|
||||
kind,
|
||||
credentials)
|
||||
}
|
||||
|
||||
func (r *credentials) FindByKind(ownerID uint64, kind types.CredentialsKind) (cc types.CredentialsSet, err error) {
|
||||
func (r *credentials) FindByKind(ownerID uint64, kind string) (cc types.CredentialsSet, err error) {
|
||||
return r.fetchSet(
|
||||
fmt.Sprintf(sqlCredentialsSelect+" AND rel_owner = ? AND kind = ?", r.tblname),
|
||||
ownerID,
|
||||
@@ -95,6 +96,12 @@ func (r *credentials) Create(c *types.Credentials) (*types.Credentials, error) {
|
||||
return c, r.db().Insert(r.tblname, c)
|
||||
}
|
||||
|
||||
func (r *credentials) Update(c *types.Credentials) (*types.Credentials, error) {
|
||||
updatedAt := time.Now()
|
||||
c.UpdatedAt = &updatedAt
|
||||
return c, r.db().Update(r.tblname, c)
|
||||
}
|
||||
|
||||
func (r *credentials) DeleteByID(id uint64) error {
|
||||
return r.updateColumnByID(r.tblname, "deleted_at", time.Now(), id)
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ func TestCredentials(t *testing.T) {
|
||||
db.Exec("DELETE FROM sys_credentials WHERE 1=1")
|
||||
|
||||
cc := types.CredentialsSet{
|
||||
&types.Credentials{OwnerID: 10000, Kind: types.CredentialsKindLinkedin, Credentials: "linkedin-profile-id"},
|
||||
&types.Credentials{OwnerID: 10000, Kind: types.CredentialsKindGPlus, Credentials: "gplus-profile-id"},
|
||||
&types.Credentials{OwnerID: 20000, Kind: types.CredentialsKindFacebook, Credentials: "facebook-profile-id"},
|
||||
&types.Credentials{OwnerID: 10000, Kind: "li", Credentials: "linkedin-profile-id"},
|
||||
&types.Credentials{OwnerID: 10000, Kind: "g+", Credentials: "gplus-profile-id"},
|
||||
&types.Credentials{OwnerID: 20000, Kind: "fb", Credentials: "facebook-profile-id"},
|
||||
}
|
||||
|
||||
for _, c := range cc {
|
||||
|
||||
@@ -38,7 +38,6 @@ func (m *MockCredentialsRepository) EXPECT() *MockCredentialsRepositoryMockRecor
|
||||
|
||||
// With mocks base method
|
||||
func (m *MockCredentialsRepository) With(ctx context.Context, db *factory.DB) repository.CredentialsRepository {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "With", ctx, db)
|
||||
ret0, _ := ret[0].(repository.CredentialsRepository)
|
||||
return ret0
|
||||
@@ -46,13 +45,11 @@ func (m *MockCredentialsRepository) With(ctx context.Context, db *factory.DB) re
|
||||
|
||||
// With indicates an expected call of With
|
||||
func (mr *MockCredentialsRepositoryMockRecorder) With(ctx, db interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "With", reflect.TypeOf((*MockCredentialsRepository)(nil).With), ctx, db)
|
||||
}
|
||||
|
||||
// FindByID mocks base method
|
||||
func (m *MockCredentialsRepository) FindByID(ID uint64) (*types.Credentials, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FindByID", ID)
|
||||
ret0, _ := ret[0].(*types.Credentials)
|
||||
ret1, _ := ret[1].(error)
|
||||
@@ -61,13 +58,11 @@ func (m *MockCredentialsRepository) FindByID(ID uint64) (*types.Credentials, err
|
||||
|
||||
// FindByID indicates an expected call of FindByID
|
||||
func (mr *MockCredentialsRepositoryMockRecorder) FindByID(ID interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByID", reflect.TypeOf((*MockCredentialsRepository)(nil).FindByID), ID)
|
||||
}
|
||||
|
||||
// FindByCredentials mocks base method
|
||||
func (m *MockCredentialsRepository) FindByCredentials(kind types.CredentialsKind, credentials string) (types.CredentialsSet, error) {
|
||||
m.ctrl.T.Helper()
|
||||
func (m *MockCredentialsRepository) FindByCredentials(kind, credentials string) (types.CredentialsSet, error) {
|
||||
ret := m.ctrl.Call(m, "FindByCredentials", kind, credentials)
|
||||
ret0, _ := ret[0].(types.CredentialsSet)
|
||||
ret1, _ := ret[1].(error)
|
||||
@@ -76,13 +71,11 @@ func (m *MockCredentialsRepository) FindByCredentials(kind types.CredentialsKind
|
||||
|
||||
// FindByCredentials indicates an expected call of FindByCredentials
|
||||
func (mr *MockCredentialsRepositoryMockRecorder) FindByCredentials(kind, credentials interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByCredentials", reflect.TypeOf((*MockCredentialsRepository)(nil).FindByCredentials), kind, credentials)
|
||||
}
|
||||
|
||||
// FindByKind mocks base method
|
||||
func (m *MockCredentialsRepository) FindByKind(ownerID uint64, kind types.CredentialsKind) (types.CredentialsSet, error) {
|
||||
m.ctrl.T.Helper()
|
||||
func (m *MockCredentialsRepository) FindByKind(ownerID uint64, kind string) (types.CredentialsSet, error) {
|
||||
ret := m.ctrl.Call(m, "FindByKind", ownerID, kind)
|
||||
ret0, _ := ret[0].(types.CredentialsSet)
|
||||
ret1, _ := ret[1].(error)
|
||||
@@ -91,13 +84,11 @@ func (m *MockCredentialsRepository) FindByKind(ownerID uint64, kind types.Creden
|
||||
|
||||
// FindByKind indicates an expected call of FindByKind
|
||||
func (mr *MockCredentialsRepositoryMockRecorder) FindByKind(ownerID, kind interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByKind", reflect.TypeOf((*MockCredentialsRepository)(nil).FindByKind), ownerID, kind)
|
||||
}
|
||||
|
||||
// FindByOwnerID mocks base method
|
||||
func (m *MockCredentialsRepository) FindByOwnerID(ownerID uint64) (types.CredentialsSet, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FindByOwnerID", ownerID)
|
||||
ret0, _ := ret[0].(types.CredentialsSet)
|
||||
ret1, _ := ret[1].(error)
|
||||
@@ -106,13 +97,11 @@ func (m *MockCredentialsRepository) FindByOwnerID(ownerID uint64) (types.Credent
|
||||
|
||||
// FindByOwnerID indicates an expected call of FindByOwnerID
|
||||
func (mr *MockCredentialsRepositoryMockRecorder) FindByOwnerID(ownerID interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByOwnerID", reflect.TypeOf((*MockCredentialsRepository)(nil).FindByOwnerID), ownerID)
|
||||
}
|
||||
|
||||
// Find mocks base method
|
||||
func (m *MockCredentialsRepository) Find() (types.CredentialsSet, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Find")
|
||||
ret0, _ := ret[0].(types.CredentialsSet)
|
||||
ret1, _ := ret[1].(error)
|
||||
@@ -121,13 +110,11 @@ func (m *MockCredentialsRepository) Find() (types.CredentialsSet, error) {
|
||||
|
||||
// Find indicates an expected call of Find
|
||||
func (mr *MockCredentialsRepositoryMockRecorder) Find() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockCredentialsRepository)(nil).Find))
|
||||
}
|
||||
|
||||
// Create mocks base method
|
||||
func (m *MockCredentialsRepository) Create(c *types.Credentials) (*types.Credentials, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Create", c)
|
||||
ret0, _ := ret[0].(*types.Credentials)
|
||||
ret1, _ := ret[1].(error)
|
||||
@@ -136,13 +123,24 @@ func (m *MockCredentialsRepository) Create(c *types.Credentials) (*types.Credent
|
||||
|
||||
// Create indicates an expected call of Create
|
||||
func (mr *MockCredentialsRepositoryMockRecorder) Create(c interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockCredentialsRepository)(nil).Create), c)
|
||||
}
|
||||
|
||||
// Update mocks base method
|
||||
func (m *MockCredentialsRepository) Update(c *types.Credentials) (*types.Credentials, error) {
|
||||
ret := m.ctrl.Call(m, "Update", c)
|
||||
ret0, _ := ret[0].(*types.Credentials)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Update indicates an expected call of Update
|
||||
func (mr *MockCredentialsRepositoryMockRecorder) Update(c interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockCredentialsRepository)(nil).Update), c)
|
||||
}
|
||||
|
||||
// DeleteByID mocks base method
|
||||
func (m *MockCredentialsRepository) DeleteByID(id uint64) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteByID", id)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
@@ -150,6 +148,5 @@ func (m *MockCredentialsRepository) DeleteByID(id uint64) error {
|
||||
|
||||
// DeleteByID indicates an expected call of DeleteByID
|
||||
func (mr *MockCredentialsRepositoryMockRecorder) DeleteByID(id interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteByID", reflect.TypeOf((*MockCredentialsRepository)(nil).DeleteByID), id)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ func (m *MockUserRepository) EXPECT() *MockUserRepositoryMockRecorder {
|
||||
|
||||
// With mocks base method
|
||||
func (m *MockUserRepository) With(ctx context.Context, db *factory.DB) repository.UserRepository {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "With", ctx, db)
|
||||
ret0, _ := ret[0].(repository.UserRepository)
|
||||
return ret0
|
||||
@@ -47,13 +46,11 @@ func (m *MockUserRepository) With(ctx context.Context, db *factory.DB) repositor
|
||||
|
||||
// With indicates an expected call of With
|
||||
func (mr *MockUserRepositoryMockRecorder) With(ctx, db interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "With", reflect.TypeOf((*MockUserRepository)(nil).With), ctx, db)
|
||||
}
|
||||
|
||||
// FindByEmail mocks base method
|
||||
func (m *MockUserRepository) FindByEmail(email string) (*types.User, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FindByEmail", email)
|
||||
ret0, _ := ret[0].(*types.User)
|
||||
ret1, _ := ret[1].(error)
|
||||
@@ -62,13 +59,11 @@ func (m *MockUserRepository) FindByEmail(email string) (*types.User, error) {
|
||||
|
||||
// FindByEmail indicates an expected call of FindByEmail
|
||||
func (mr *MockUserRepositoryMockRecorder) FindByEmail(email interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByEmail", reflect.TypeOf((*MockUserRepository)(nil).FindByEmail), email)
|
||||
}
|
||||
|
||||
// FindByUsername mocks base method
|
||||
func (m *MockUserRepository) FindByUsername(username string) (*types.User, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FindByUsername", username)
|
||||
ret0, _ := ret[0].(*types.User)
|
||||
ret1, _ := ret[1].(error)
|
||||
@@ -77,13 +72,11 @@ func (m *MockUserRepository) FindByUsername(username string) (*types.User, error
|
||||
|
||||
// FindByUsername indicates an expected call of FindByUsername
|
||||
func (mr *MockUserRepositoryMockRecorder) FindByUsername(username interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByUsername", reflect.TypeOf((*MockUserRepository)(nil).FindByUsername), username)
|
||||
}
|
||||
|
||||
// FindByID mocks base method
|
||||
func (m *MockUserRepository) FindByID(id uint64) (*types.User, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FindByID", id)
|
||||
ret0, _ := ret[0].(*types.User)
|
||||
ret1, _ := ret[1].(error)
|
||||
@@ -92,13 +85,11 @@ func (m *MockUserRepository) FindByID(id uint64) (*types.User, error) {
|
||||
|
||||
// FindByID indicates an expected call of FindByID
|
||||
func (mr *MockUserRepositoryMockRecorder) FindByID(id interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByID", reflect.TypeOf((*MockUserRepository)(nil).FindByID), id)
|
||||
}
|
||||
|
||||
// FindByIDs mocks base method
|
||||
func (m *MockUserRepository) FindByIDs(id ...uint64) (types.UserSet, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{}
|
||||
for _, a := range id {
|
||||
varargs = append(varargs, a)
|
||||
@@ -111,28 +102,11 @@ func (m *MockUserRepository) FindByIDs(id ...uint64) (types.UserSet, error) {
|
||||
|
||||
// FindByIDs indicates an expected call of FindByIDs
|
||||
func (mr *MockUserRepositoryMockRecorder) FindByIDs(id ...interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByIDs", reflect.TypeOf((*MockUserRepository)(nil).FindByIDs), id...)
|
||||
}
|
||||
|
||||
// FindBySatosaID mocks base method
|
||||
func (m *MockUserRepository) FindBySatosaID(id string) (*types.User, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FindBySatosaID", id)
|
||||
ret0, _ := ret[0].(*types.User)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// FindBySatosaID indicates an expected call of FindBySatosaID
|
||||
func (mr *MockUserRepositoryMockRecorder) FindBySatosaID(id interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindBySatosaID", reflect.TypeOf((*MockUserRepository)(nil).FindBySatosaID), id)
|
||||
}
|
||||
|
||||
// Find mocks base method
|
||||
func (m *MockUserRepository) Find(filter *types.UserFilter) ([]*types.User, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Find", filter)
|
||||
ret0, _ := ret[0].([]*types.User)
|
||||
ret1, _ := ret[1].(error)
|
||||
@@ -141,13 +115,11 @@ func (m *MockUserRepository) Find(filter *types.UserFilter) ([]*types.User, erro
|
||||
|
||||
// Find indicates an expected call of Find
|
||||
func (mr *MockUserRepositoryMockRecorder) Find(filter interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockUserRepository)(nil).Find), filter)
|
||||
}
|
||||
|
||||
// Create mocks base method
|
||||
func (m *MockUserRepository) Create(mod *types.User) (*types.User, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Create", mod)
|
||||
ret0, _ := ret[0].(*types.User)
|
||||
ret1, _ := ret[1].(error)
|
||||
@@ -156,13 +128,11 @@ func (m *MockUserRepository) Create(mod *types.User) (*types.User, error) {
|
||||
|
||||
// Create indicates an expected call of Create
|
||||
func (mr *MockUserRepositoryMockRecorder) Create(mod interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockUserRepository)(nil).Create), mod)
|
||||
}
|
||||
|
||||
// Update mocks base method
|
||||
func (m *MockUserRepository) Update(mod *types.User) (*types.User, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Update", mod)
|
||||
ret0, _ := ret[0].(*types.User)
|
||||
ret1, _ := ret[1].(error)
|
||||
@@ -171,13 +141,11 @@ func (m *MockUserRepository) Update(mod *types.User) (*types.User, error) {
|
||||
|
||||
// Update indicates an expected call of Update
|
||||
func (mr *MockUserRepositoryMockRecorder) Update(mod interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUserRepository)(nil).Update), mod)
|
||||
}
|
||||
|
||||
// BindAvatar mocks base method
|
||||
func (m *MockUserRepository) BindAvatar(user *types.User, avatar io.Reader) (*types.User, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "BindAvatar", user, avatar)
|
||||
ret0, _ := ret[0].(*types.User)
|
||||
ret1, _ := ret[1].(error)
|
||||
@@ -186,13 +154,11 @@ func (m *MockUserRepository) BindAvatar(user *types.User, avatar io.Reader) (*ty
|
||||
|
||||
// BindAvatar indicates an expected call of BindAvatar
|
||||
func (mr *MockUserRepositoryMockRecorder) BindAvatar(user, avatar interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BindAvatar", reflect.TypeOf((*MockUserRepository)(nil).BindAvatar), user, avatar)
|
||||
}
|
||||
|
||||
// SuspendByID mocks base method
|
||||
func (m *MockUserRepository) SuspendByID(id uint64) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SuspendByID", id)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
@@ -200,13 +166,11 @@ func (m *MockUserRepository) SuspendByID(id uint64) error {
|
||||
|
||||
// SuspendByID indicates an expected call of SuspendByID
|
||||
func (mr *MockUserRepositoryMockRecorder) SuspendByID(id interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SuspendByID", reflect.TypeOf((*MockUserRepository)(nil).SuspendByID), id)
|
||||
}
|
||||
|
||||
// UnsuspendByID mocks base method
|
||||
func (m *MockUserRepository) UnsuspendByID(id uint64) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UnsuspendByID", id)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
@@ -214,13 +178,11 @@ func (m *MockUserRepository) UnsuspendByID(id uint64) error {
|
||||
|
||||
// UnsuspendByID indicates an expected call of UnsuspendByID
|
||||
func (mr *MockUserRepositoryMockRecorder) UnsuspendByID(id interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnsuspendByID", reflect.TypeOf((*MockUserRepository)(nil).UnsuspendByID), id)
|
||||
}
|
||||
|
||||
// DeleteByID mocks base method
|
||||
func (m *MockUserRepository) DeleteByID(id uint64) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteByID", id)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
@@ -228,6 +190,5 @@ func (m *MockUserRepository) DeleteByID(id uint64) error {
|
||||
|
||||
// DeleteByID indicates an expected call of DeleteByID
|
||||
func (mr *MockUserRepositoryMockRecorder) DeleteByID(id interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteByID", reflect.TypeOf((*MockUserRepository)(nil).DeleteByID), id)
|
||||
}
|
||||
|
||||
@@ -160,6 +160,12 @@ func (r *role) Reset() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove all rules for both roles
|
||||
sql = "DELETE FROM sys_rules WHERE rel_role IN (1, 2)"
|
||||
if _, err = r.db().Exec(sql); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Value: Allow (2), Deny (1), Inherit(0)
|
||||
sql = `REPLACE INTO sys_rules (rel_role, resource, operation, value) VALUES
|
||||
-- Everyone
|
||||
|
||||
@@ -20,7 +20,6 @@ type (
|
||||
FindByUsername(username string) (*types.User, error)
|
||||
FindByID(id uint64) (*types.User, error)
|
||||
FindByIDs(id ...uint64) (types.UserSet, error)
|
||||
FindBySatosaID(id string) (*types.User, error)
|
||||
Find(filter *types.UserFilter) ([]*types.User, error)
|
||||
|
||||
Create(mod *types.User) (*types.User, error)
|
||||
@@ -42,8 +41,8 @@ type (
|
||||
)
|
||||
|
||||
const (
|
||||
sqlUserColumns = "id, email, username, password, name, handle, " +
|
||||
"meta, satosa_id, rel_organisation, " +
|
||||
sqlUserColumns = "id, email, username, name, handle, " +
|
||||
"meta, rel_organisation, " +
|
||||
"created_at, updated_at, suspended_at, deleted_at"
|
||||
sqlUserScope = "suspended_at IS NULL AND deleted_at IS NULL"
|
||||
sqlUserSelect = "SELECT " + sqlUserColumns + " FROM %s WHERE " + sqlUserScope
|
||||
@@ -69,13 +68,6 @@ func (r *user) FindByUsername(username string) (*types.User, error) {
|
||||
return mod, isFound(r.db().Get(mod, sql, username), mod.ID > 0, ErrUserNotFound)
|
||||
}
|
||||
|
||||
func (r *user) FindBySatosaID(satosaID string) (*types.User, error) {
|
||||
sql := fmt.Sprintf(sqlUserSelect, r.users) + " AND satosa_id = ?"
|
||||
mod := &types.User{}
|
||||
|
||||
return mod, isFound(r.db().Get(mod, sql, satosaID), mod.ID > 0, ErrUserNotFound)
|
||||
}
|
||||
|
||||
func (r *user) FindByEmail(email string) (*types.User, error) {
|
||||
sql := fmt.Sprintf(sqlUserSelect, r.users) + " AND email = ?"
|
||||
mod := &types.User{}
|
||||
|
||||
@@ -27,7 +27,6 @@ func TestUser(t *testing.T) {
|
||||
user := &types.User{
|
||||
Name: "John User Doe",
|
||||
Username: "johndoe",
|
||||
SatosaID: "1234",
|
||||
Meta: &types.UserMeta{
|
||||
Avatar: "123",
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
"github.com/pkg/errors"
|
||||
@@ -23,7 +24,7 @@ type (
|
||||
AuthService interface {
|
||||
With(ctx context.Context) AuthService
|
||||
|
||||
Social(profile goth.User) (*types.User, error)
|
||||
External(profile goth.User) (*types.User, error)
|
||||
|
||||
CheckPassword(username, password string) (*types.User, error)
|
||||
ChangePassword(user *types.User, password string) error
|
||||
@@ -50,14 +51,11 @@ func (svc *auth) With(ctx context.Context) AuthService {
|
||||
// Social user verifies existance by using email value from social profile and creates user if needed
|
||||
//
|
||||
// It does not update user's info
|
||||
func (svc *auth) Social(profile goth.User) (u *types.User, err error) {
|
||||
var kind types.CredentialsKind
|
||||
func (svc *auth) External(profile goth.User) (u *types.User, err error) {
|
||||
var lastUsedAt = time.Now()
|
||||
|
||||
switch profile.Provider {
|
||||
case "facebook", "gplus", "github", "linkedin":
|
||||
kind = types.CredentialsKind(profile.Provider)
|
||||
default:
|
||||
return nil, errors.New("Unsupported provider")
|
||||
if _, err := goth.GetProvider(profile.Provider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if profile.Email == "" {
|
||||
@@ -66,7 +64,7 @@ func (svc *auth) Social(profile goth.User) (u *types.User, err error) {
|
||||
|
||||
return u, svc.db.Transaction(func() error {
|
||||
var c *types.Credentials
|
||||
if cc, err := svc.credentials.FindByCredentials(kind, profile.UserID); err == nil {
|
||||
if cc, err := svc.credentials.FindByCredentials(profile.Provider, profile.UserID); err == nil {
|
||||
// Credentials found, load user
|
||||
for _, c := range cc {
|
||||
if !c.Valid() {
|
||||
@@ -110,6 +108,8 @@ func (svc *auth) Social(profile goth.User) (u *types.User, err error) {
|
||||
findByEmail:
|
||||
// Find user via his email
|
||||
if u, err = svc.users.FindByEmail(profile.Email); err == repository.ErrUserNotFound {
|
||||
// @todo check if it is ok to auto-create a user here
|
||||
|
||||
// In case we do not have this email, create a new user
|
||||
u = &types.User{
|
||||
Email: profile.Email,
|
||||
@@ -119,13 +119,35 @@ func (svc *auth) Social(profile goth.User) (u *types.User, err error) {
|
||||
}
|
||||
|
||||
if u, err = svc.users.Create(u); err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "could not create user after successful social auth")
|
||||
}
|
||||
|
||||
log.Printf("Created new user after successful social auth (%v, %v)", u.ID, u.Email)
|
||||
|
||||
// Owner created
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else if !u.Valid() {
|
||||
return errors.Errorf(
|
||||
"Social login to an invalid/suspended user (user ID: %v)",
|
||||
u.ID,
|
||||
)
|
||||
} else {
|
||||
log.Printf(
|
||||
"Autheticated user (%v, %v) via %s, existing user",
|
||||
u.ID,
|
||||
u.Email,
|
||||
profile.Provider,
|
||||
)
|
||||
}
|
||||
|
||||
if c == nil {
|
||||
c = &types.Credentials{
|
||||
Kind: kind,
|
||||
Kind: profile.Provider,
|
||||
OwnerID: u.ID,
|
||||
Credentials: profile.UserID,
|
||||
LastUsedAt: &lastUsedAt,
|
||||
}
|
||||
|
||||
if !profile.ExpiresAt.IsZero() {
|
||||
@@ -138,31 +160,32 @@ func (svc *auth) Social(profile goth.User) (u *types.User, err error) {
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
"Autheticated user (%v, %v) via %s, created user and credentials (%v)",
|
||||
"Creating new credential entry (%v, %v) for exisintg user (%v, %v)",
|
||||
c.ID,
|
||||
profile.Provider,
|
||||
u.ID,
|
||||
u.Email,
|
||||
profile.Provider,
|
||||
c.ID,
|
||||
)
|
||||
} else {
|
||||
if !profile.ExpiresAt.IsZero() {
|
||||
// Copy expiration date when provided
|
||||
c.ExpiresAt = &profile.ExpiresAt
|
||||
}
|
||||
|
||||
// Owner created
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else if !u.Valid() {
|
||||
return errors.Errorf(
|
||||
"Social login to an invalid/suspended user (user ID: %v)",
|
||||
c.LastUsedAt = &lastUsedAt
|
||||
if c, err = svc.credentials.Update(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
"Updating credential entry (%v, %v) for exisintg user (%v, %v)",
|
||||
c.ID,
|
||||
profile.Provider,
|
||||
u.ID,
|
||||
u.Email,
|
||||
)
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
"Autheticated user (%v, %v) via %s, existing user",
|
||||
u.ID,
|
||||
u.Email,
|
||||
profile.Provider,
|
||||
)
|
||||
|
||||
// Owner loaded, carry on.
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestSocialSigninWithExistingCredentials(t *testing.T) {
|
||||
|
||||
crdRpoMock := repomock.NewMockCredentialsRepository(mockCtrl)
|
||||
crdRpoMock.EXPECT().
|
||||
FindByCredentials(types.CredentialsKindGPlus, p.UserID).
|
||||
FindByCredentials("g+", p.UserID).
|
||||
Times(1).
|
||||
Return(types.CredentialsSet{c}, nil)
|
||||
|
||||
@@ -54,12 +54,12 @@ func TestSocialSigninWithNewUserCredentials(t *testing.T) {
|
||||
|
||||
crdRpoMock := repomock.NewMockCredentialsRepository(mockCtrl)
|
||||
crdRpoMock.EXPECT().
|
||||
FindByCredentials(types.CredentialsKindGPlus, p.UserID).
|
||||
FindByCredentials("g+", p.UserID).
|
||||
Times(1).
|
||||
Return(types.CredentialsSet{}, nil)
|
||||
|
||||
crdRpoMock.EXPECT().
|
||||
Create(&types.Credentials{Kind: types.CredentialsKindGPlus, OwnerID: u.ID, Credentials: p.UserID}).
|
||||
Create(&types.Credentials{Kind: "g+", OwnerID: u.ID, Credentials: p.UserID}).
|
||||
Times(1).
|
||||
Return(c, nil)
|
||||
|
||||
|
||||
@@ -37,8 +37,6 @@ type (
|
||||
FindByIDs(id ...uint64) (types.UserSet, error)
|
||||
Find(filter *types.UserFilter) (types.UserSet, error)
|
||||
|
||||
FindOrCreate(*types.User) (*types.User, error)
|
||||
|
||||
Create(input *types.User) (*types.User, error)
|
||||
Update(mod *types.User) (*types.User, error)
|
||||
|
||||
@@ -85,36 +83,6 @@ func (svc *user) Find(filter *types.UserFilter) (types.UserSet, error) {
|
||||
return svc.user.Find(filter)
|
||||
}
|
||||
|
||||
// FindOrCreate finds existing or creates new user wrapped in db transaction
|
||||
func (svc *user) FindOrCreate(user *types.User) (out *types.User, err error) {
|
||||
return out, svc.db.Transaction(func() error {
|
||||
if len(user.SatosaID) != uuidLength {
|
||||
// @todo uuid format check
|
||||
return errors.Errorf("Invalid UUID value (%v) for SATOSA ID", user.SatosaID)
|
||||
}
|
||||
|
||||
out, err = svc.user.FindBySatosaID(user.SatosaID)
|
||||
|
||||
if err == repository.ErrUserNotFound {
|
||||
// @todo do we allow autocreation of nonexisting users?
|
||||
out, err = svc.user.Create(user)
|
||||
return err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// FindBySatosaID error
|
||||
return err
|
||||
}
|
||||
|
||||
out, err = svc.user.Update(out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (svc *user) Create(input *types.User) (out *types.User, err error) {
|
||||
return out, svc.db.Transaction(func() error {
|
||||
if !svc.prm.CanCreateUser() {
|
||||
@@ -124,6 +92,7 @@ func (svc *user) Create(input *types.User) (out *types.User, err error) {
|
||||
if out, err = svc.user.Create(input); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
+19
-17
@@ -79,7 +79,7 @@ func (ctrl *Auth) Handlers(jwtEncoder auth.TokenEncoder) *handlers.Auth {
|
||||
}
|
||||
h.Login = func(w http.ResponseWriter, r *http.Request) {
|
||||
params := request.NewAuthLogin()
|
||||
ctx := r.Context()
|
||||
// ctx := r.Context()
|
||||
|
||||
// parse request to fill parameters
|
||||
err := params.Fill(r)
|
||||
@@ -88,26 +88,28 @@ func (ctrl *Auth) Handlers(jwtEncoder auth.TokenEncoder) *handlers.Auth {
|
||||
return
|
||||
}
|
||||
|
||||
userSvc := service.User(ctx)
|
||||
// userSvc := service.User(ctx)
|
||||
|
||||
// check email and username for login
|
||||
user, err := userSvc.FindByEmail(params.Username)
|
||||
if err != nil {
|
||||
user, err = userSvc.FindByUsername(params.Username)
|
||||
}
|
||||
|
||||
// can't find user
|
||||
if err != nil {
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
// @todo disabled until we migrate to local users through credentials
|
||||
// user, err := userSvc.FindByEmail(params.Username)
|
||||
// if err != nil {
|
||||
// user, err = userSvc.FindByUsername(params.Username)
|
||||
// }
|
||||
//
|
||||
// // can't find user
|
||||
// if err != nil {
|
||||
// resputil.JSON(w, err)
|
||||
// return
|
||||
// }
|
||||
|
||||
// validate password
|
||||
if user.ValidatePassword(params.Password) {
|
||||
jwtEncoder.SetCookie(w, r, user)
|
||||
resputil.JSON(w, user, err)
|
||||
return
|
||||
}
|
||||
// @todo disabled until we migrate to local users through credentials
|
||||
// if user.ValidatePassword(params.Password) {
|
||||
// jwtEncoder.SetCookie(w, r, user)
|
||||
// resputil.JSON(w, user, err)
|
||||
// return
|
||||
// }
|
||||
|
||||
resputil.JSON(w, errors.New("Password doesn't match"))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/gothic"
|
||||
"github.com/titpetric/factory/resputil"
|
||||
|
||||
"github.com/crusttech/crust/internal/auth"
|
||||
"github.com/crusttech/crust/system/internal/service"
|
||||
)
|
||||
|
||||
type (
|
||||
ExternalAuth struct {
|
||||
auth service.AuthService
|
||||
jwtEncoder auth.TokenEncoder
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
externalAuthBaseUrl = "/auth/external"
|
||||
)
|
||||
|
||||
func NewSocial(jwtEncoder auth.TokenEncoder) *ExternalAuth {
|
||||
return &ExternalAuth{
|
||||
auth: service.DefaultAuth,
|
||||
jwtEncoder: jwtEncoder,
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl *ExternalAuth) MountRoutes(r chi.Router) {
|
||||
|
||||
// Make sure we're backwards compatible and redirect /oidc to /auth/external/openid-connect-didmos2
|
||||
r.Get("/oidc", func(w http.ResponseWriter, req *http.Request) {
|
||||
http.Redirect(w, req, externalAuthBaseUrl+"/openid-connect-didmos2", http.StatusMovedPermanently)
|
||||
})
|
||||
|
||||
// Copy provider from path (Chi URL param) to request context and return it
|
||||
copyProviderToContext := func(r *http.Request) *http.Request {
|
||||
return r.WithContext(context.WithValue(r.Context(), "provider", chi.URLParam(r, "provider")))
|
||||
}
|
||||
|
||||
r.Route(externalAuthBaseUrl+"/{provider}", func(r chi.Router) {
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
r = copyProviderToContext(r)
|
||||
|
||||
// Always set redir cookie, even if not requested. If param is empty, cookie is removed
|
||||
ctrl.setSessionCookie(w, r, "redir", r.URL.Query().Get("redir"))
|
||||
|
||||
// try to get the user without re-authenticating
|
||||
if user, err := gothic.CompleteUserAuth(w, r); err != nil {
|
||||
gothic.BeginAuthHandler(w, r)
|
||||
} else {
|
||||
// We've successfully singed-in through 3rd party auth
|
||||
ctrl.handleSuccessfulAuth(w, r, user)
|
||||
}
|
||||
})
|
||||
|
||||
r.Get("/callback", func(w http.ResponseWriter, r *http.Request) {
|
||||
r = copyProviderToContext(r)
|
||||
|
||||
if user, err := gothic.CompleteUserAuth(w, r); err != nil {
|
||||
log.Printf("Failed to complete user auth: %v", err)
|
||||
ctrl.handleFailedCallback(w, r, err)
|
||||
} else {
|
||||
ctrl.handleSuccessfulAuth(w, r, user)
|
||||
}
|
||||
})
|
||||
|
||||
r.Get("/logout", func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := gothic.Logout(w, r); err != nil {
|
||||
log.Printf("Failed to external logout: %v", err)
|
||||
}
|
||||
|
||||
w.Header().Set("Location", "/")
|
||||
w.WriteHeader(http.StatusTemporaryRedirect)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (ctrl *ExternalAuth) handleFailedCallback(w http.ResponseWriter, r *http.Request, err error) {
|
||||
provider := chi.URLParam(r, "provider")
|
||||
|
||||
if strings.Contains(err.Error(), "Error processing your OAuth request: Invalid oauth_verifier parameter") {
|
||||
// Just take user through the same loop again
|
||||
w.Header().Set("Location", externalAuthBaseUrl+"/"+provider)
|
||||
w.WriteHeader(http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "SSO Error: %v", err.Error())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// Handles authentication via external auth providers of
|
||||
// unknown an user + appending authentication on external providers
|
||||
// to a current user
|
||||
func (ctrl *ExternalAuth) handleSuccessfulAuth(w http.ResponseWriter, r *http.Request, cred goth.User) {
|
||||
log.Printf("Successful external login: %v", cred)
|
||||
|
||||
if u, err := ctrl.auth.With(r.Context()).External(cred); err != nil {
|
||||
resputil.JSON(w, err)
|
||||
} else {
|
||||
ctrl.jwtEncoder.SetCookie(w, r, u)
|
||||
|
||||
if c, err := r.Cookie("redir"); c != nil && err == nil {
|
||||
ctrl.setSessionCookie(w, r, "redir", "")
|
||||
w.Header().Set("Location", c.Value)
|
||||
w.WriteHeader(http.StatusSeeOther)
|
||||
|
||||
}
|
||||
|
||||
resputil.JSON(w, u, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Extracts and authenticates JWT from context, validates claims
|
||||
func (ctrl *ExternalAuth) setSessionCookie(w http.ResponseWriter, r *http.Request, name, value string) {
|
||||
cookie := &http.Cookie{
|
||||
Name: name,
|
||||
Secure: r.URL.Scheme == "https",
|
||||
Domain: r.URL.Hostname(),
|
||||
Path: externalAuthBaseUrl,
|
||||
}
|
||||
|
||||
if value == "" {
|
||||
cookie.Expires = time.Unix(0, 0)
|
||||
} else {
|
||||
cookie.Value = value
|
||||
}
|
||||
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/titpetric/factory/resputil"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/crusttech/go-oidc"
|
||||
|
||||
"github.com/crusttech/crust/internal/auth"
|
||||
"github.com/crusttech/crust/internal/config"
|
||||
"github.com/crusttech/crust/system/internal/repository"
|
||||
"github.com/crusttech/crust/system/internal/service"
|
||||
"github.com/crusttech/crust/system/types"
|
||||
)
|
||||
|
||||
const DB_SETTINGS_KEY_OIDC_CLIENT = "oidc-client"
|
||||
|
||||
type (
|
||||
openIdConnect struct {
|
||||
provider *oidc.Provider
|
||||
verifier *oidc.IDTokenVerifier
|
||||
config oauth2.Config
|
||||
|
||||
appURL string
|
||||
stateCookieExpiry int64
|
||||
stateCookieDomain string
|
||||
|
||||
userService service.UserService
|
||||
|
||||
jwt auth.TokenEncoder
|
||||
}
|
||||
|
||||
oidcProfile struct {
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Sub string `json:"sub"`
|
||||
}
|
||||
)
|
||||
|
||||
const openIdConnectStateCookie = "oidc-state"
|
||||
|
||||
// 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 auth.TokenEncoder, settings repository.Settings) (c *openIdConnect, err error) {
|
||||
c = &openIdConnect{
|
||||
appURL: cfg.AppURL,
|
||||
stateCookieExpiry: cfg.StateCookieExpiry,
|
||||
stateCookieDomain: cfg.StateCookieDomain,
|
||||
userService: usvc,
|
||||
jwt: jwt,
|
||||
}
|
||||
|
||||
// Allow 5 seconds for issuer discovery process
|
||||
c.provider, err = oidc.NewProvider(ctx, cfg.Issuer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
} else {
|
||||
client := &oidc.Client{}
|
||||
|
||||
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 || client == nil {
|
||||
log.Printf("OIDC client not found, registering new")
|
||||
|
||||
// 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 != nil {
|
||||
return nil, errors.Wrap(err, "could not register OIDC client")
|
||||
}
|
||||
|
||||
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.config.Scopes = []string{
|
||||
oidc.ScopeOpenID,
|
||||
"email",
|
||||
"profile",
|
||||
"address",
|
||||
"phone_number",
|
||||
}
|
||||
|
||||
c.verifier = c.provider.Verifier(&oidc.Config{ClientID: c.config.ClientID})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Redirects user to the issuer's login screen
|
||||
func (c *openIdConnect) HandleRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
// @todo sure we can improve this...
|
||||
rand.Seed(4321)
|
||||
var state = strconv.FormatInt(rand.Int63(), 10)
|
||||
|
||||
// Store state to cookie as well
|
||||
c.setStateCookie(w, r, state)
|
||||
|
||||
http.Redirect(w, r, c.config.AuthCodeURL(state), http.StatusFound)
|
||||
}
|
||||
|
||||
// Handles callback from issuer
|
||||
//
|
||||
// If everything goes well (scope & token verification) it reads issued claims,
|
||||
// creates Crust JWT and stores it in a cookie.
|
||||
//
|
||||
// @todo All failed responses must redirect to appURL as well + some error code that will be displayed on the client
|
||||
func (c *openIdConnect) HandleOAuth2Callback(w http.ResponseWriter, r *http.Request) {
|
||||
var ctx = r.Context()
|
||||
|
||||
if !c.stateCheck(r) {
|
||||
resputil.JSON(w, "State check failed")
|
||||
return
|
||||
}
|
||||
|
||||
c.setStateCookie(w, r, "") // remove state cookie
|
||||
|
||||
oauth2Token, err := c.config.Exchange(ctx, r.URL.Query().Get("code"))
|
||||
if err != nil {
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract the ID Token from OAuth2 token.
|
||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse and verify ID Token payload.
|
||||
_, err = c.verifier.Verify(ctx, rawIDToken)
|
||||
if err != nil {
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
u, err := c.provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token))
|
||||
if err != nil {
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
p := &oidcProfile{}
|
||||
u.Claims(p)
|
||||
|
||||
var user = &types.User{
|
||||
SatosaID: p.Sub,
|
||||
Email: p.Email,
|
||||
Name: p.Name,
|
||||
}
|
||||
|
||||
if user, err = c.userService.With(ctx).FindOrCreate(user); err != nil {
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
} else {
|
||||
c.jwt.SetCookie(w, r, user)
|
||||
}
|
||||
|
||||
http.Redirect(w, r, c.appURL, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (c *openIdConnect) stateCheck(r *http.Request) bool {
|
||||
if cState, err := r.Cookie(openIdConnectStateCookie); err == nil {
|
||||
rState := r.URL.Query().Get("state")
|
||||
return len(rState) > 0 && cState.Value == rState
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Sets state cookie
|
||||
func (c *openIdConnect) setStateCookie(w http.ResponseWriter, r *http.Request, value string) {
|
||||
var maxAge int
|
||||
|
||||
if len(value) == 0 {
|
||||
// When empty string for a value is received,
|
||||
// set maxAge to -1. That will effectively delete the cookie
|
||||
maxAge = -1
|
||||
}
|
||||
|
||||
// Store state to cookie as well
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: openIdConnectStateCookie,
|
||||
Value: value,
|
||||
Expires: time.Now().Add(time.Duration(c.stateCookieExpiry) * time.Minute),
|
||||
MaxAge: maxAge,
|
||||
HttpOnly: true,
|
||||
Secure: r.URL.Scheme == "https",
|
||||
Path: "/oidc",
|
||||
Domain: c.stateCookieDomain,
|
||||
})
|
||||
}
|
||||
+2
-29
@@ -1,43 +1,16 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
|
||||
"github.com/crusttech/crust/internal/auth"
|
||||
"github.com/crusttech/crust/internal/config"
|
||||
"github.com/crusttech/crust/system/internal/repository"
|
||||
"github.com/crusttech/crust/system/internal/service"
|
||||
"github.com/crusttech/crust/system/rest/handlers"
|
||||
)
|
||||
|
||||
func MountRoutes(oidcConfig *config.OIDC, socialConfig *config.Social, jwtEncoder auth.TokenEncoder) func(chi.Router) {
|
||||
var err error
|
||||
var userSvc = service.User(context.Background())
|
||||
var ctx = context.Background()
|
||||
var oidc *openIdConnect
|
||||
|
||||
if oidcConfig.Enabled {
|
||||
oidc, err = OpenIdConnect(ctx, oidcConfig, userSvc, jwtEncoder, repository.NewSettings(ctx, repository.DB(ctx)))
|
||||
if err != nil {
|
||||
log.Println("Could not initialize OIDC:", err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Println("OIDC is disabled")
|
||||
}
|
||||
|
||||
func MountRoutes(jwtEncoder auth.TokenEncoder) func(chi.Router) {
|
||||
// Initialize handers & controllers.
|
||||
return func(r chi.Router) {
|
||||
if oidcConfig.Enabled && oidc != nil {
|
||||
r.Route("/oidc", func(r chi.Router) {
|
||||
r.Get("/", oidc.HandleRedirect)
|
||||
r.Get("/callback", oidc.HandleOAuth2Callback)
|
||||
})
|
||||
}
|
||||
|
||||
NewSocial(socialConfig, jwtEncoder).MountRoutes(r)
|
||||
NewSocial(jwtEncoder).MountRoutes(r)
|
||||
|
||||
// Provide raw `/auth` handlers
|
||||
Auth{}.New().Handlers(jwtEncoder).MountRoutes(r)
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/gothic"
|
||||
"github.com/markbates/goth/providers/facebook"
|
||||
"github.com/markbates/goth/providers/github"
|
||||
"github.com/markbates/goth/providers/gplus"
|
||||
"github.com/markbates/goth/providers/linkedin"
|
||||
"github.com/titpetric/factory/resputil"
|
||||
|
||||
"github.com/crusttech/crust/internal/auth"
|
||||
"github.com/crusttech/crust/internal/config"
|
||||
"github.com/crusttech/crust/system/internal/service"
|
||||
)
|
||||
|
||||
type (
|
||||
Social struct {
|
||||
auth service.AuthService
|
||||
config *config.Social
|
||||
jwtEncoder auth.TokenEncoder
|
||||
}
|
||||
)
|
||||
|
||||
func NewSocial(config *config.Social, jwtEncoder auth.TokenEncoder) *Social {
|
||||
return &Social{
|
||||
auth: service.DefaultAuth,
|
||||
config: config,
|
||||
jwtEncoder: jwtEncoder,
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl *Social) MountRoutes(r chi.Router) {
|
||||
store := sessions.NewCookieStore([]byte(ctrl.config.SessionStoreSecret))
|
||||
store.MaxAge(ctrl.config.SessionStoreExpiry)
|
||||
store.Options.Path = "/social"
|
||||
store.Options.HttpOnly = true
|
||||
store.Options.Secure = false // @todo
|
||||
gothic.Store = store
|
||||
|
||||
getProviderConfig := func(provider string) (key, secret string, has bool) {
|
||||
key, keyOk := os.LookupEnv("AUTH_SOCIAL_" + provider + "_KEY")
|
||||
sec, secOk := os.LookupEnv("AUTH_SOCIAL_" + provider + "_SECRET")
|
||||
if keyOk && secOk {
|
||||
return key, sec, true
|
||||
} else {
|
||||
log.Print(
|
||||
"Binding auth endpoints without " + provider + " provider " +
|
||||
"(missing key/secret, check AUTH_" + provider + "_KEY, AUTH_" + provider + "_SECRET)")
|
||||
}
|
||||
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
if key, sec, ok := getProviderConfig("FACEBOOK"); ok {
|
||||
goth.UseProviders(facebook.New(key, sec, ctrl.config.Url+"/social/facebook/callback", "email"))
|
||||
}
|
||||
|
||||
if key, sec, ok := getProviderConfig("GPLUS"); ok {
|
||||
goth.UseProviders(gplus.New(key, sec, ctrl.config.Url+"/social/gplus/callback", "email"))
|
||||
}
|
||||
|
||||
if key, sec, ok := getProviderConfig("GITHUB"); ok {
|
||||
goth.UseProviders(github.New(key, sec, ctrl.config.Url+"/social/gplus/callback", "email"))
|
||||
}
|
||||
|
||||
if key, sec, ok := getProviderConfig("LINKEDIN"); ok {
|
||||
goth.UseProviders(linkedin.New(key, sec, ctrl.config.Url+"/social/linkedin/callback", "email"))
|
||||
}
|
||||
|
||||
// Copy provider from path (Chi URL param) to request context and return it
|
||||
copyProviderToContext := func(r *http.Request) *http.Request {
|
||||
return r.WithContext(context.WithValue(r.Context(), "provider", chi.URLParam(r, "provider")))
|
||||
}
|
||||
|
||||
r.Route("/social/{provider}", func(r chi.Router) {
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
r = copyProviderToContext(r)
|
||||
|
||||
// Always set redir cookie, even if not requested. If param is empty, cookie is removed
|
||||
ctrl.setCookie(w, r, "redir", r.URL.Query().Get("redir"))
|
||||
|
||||
// try to get the user without re-authenticating
|
||||
if user, err := gothic.CompleteUserAuth(w, r); err != nil {
|
||||
gothic.BeginAuthHandler(w, r)
|
||||
} else {
|
||||
// We've successfully singed-in through 3rd party auth
|
||||
ctrl.handleSuccessfulAuth(w, r, user)
|
||||
}
|
||||
})
|
||||
|
||||
r.Get("/callback", func(w http.ResponseWriter, r *http.Request) {
|
||||
r = copyProviderToContext(r)
|
||||
|
||||
if user, err := gothic.CompleteUserAuth(w, r); err != nil {
|
||||
log.Printf("Failed to complete user auth: %v", err)
|
||||
ctrl.handleFailedCallback(w, r, err)
|
||||
} else {
|
||||
ctrl.handleSuccessfulAuth(w, r, user)
|
||||
}
|
||||
})
|
||||
|
||||
r.Get("/logout", func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := gothic.Logout(w, r); err != nil {
|
||||
log.Printf("Failed to social logout: %v", err)
|
||||
}
|
||||
|
||||
w.Header().Set("Location", "/")
|
||||
w.WriteHeader(http.StatusTemporaryRedirect)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (ctrl *Social) handleFailedCallback(w http.ResponseWriter, r *http.Request, err error) {
|
||||
provider := chi.URLParam(r, "provider")
|
||||
|
||||
if strings.Contains(err.Error(), "Error processing your OAuth request: Invalid oauth_verifier parameter") {
|
||||
// Just take user through the same loop again
|
||||
w.Header().Set("Location", "/social/"+provider)
|
||||
w.WriteHeader(http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "SSO Error: %v", err.Error())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// Handles authentication via external auth providers of
|
||||
// unknown an user + appending authentication on external providers
|
||||
// to a current user
|
||||
func (ctrl *Social) handleSuccessfulAuth(w http.ResponseWriter, r *http.Request, cred goth.User) {
|
||||
log.Printf("Successful social login: %v", cred)
|
||||
|
||||
if u, err := ctrl.auth.With(r.Context()).Social(cred); err != nil {
|
||||
resputil.JSON(w, err)
|
||||
} else {
|
||||
ctrl.jwtEncoder.SetCookie(w, r, u)
|
||||
|
||||
if c, err := r.Cookie("redir"); c != nil && err == nil {
|
||||
ctrl.setCookie(w, r, "redir", "")
|
||||
w.Header().Set("Location", c.Value)
|
||||
w.WriteHeader(http.StatusSeeOther)
|
||||
|
||||
}
|
||||
|
||||
resputil.JSON(w, u, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Extracts and authenticates JWT from context, validates claims
|
||||
func (ctrl *Social) setCookie(w http.ResponseWriter, r *http.Request, name, value string) {
|
||||
cookie := &http.Cookie{
|
||||
Name: name,
|
||||
Expires: time.Now().Add(time.Duration(ctrl.config.SessionStoreExpiry) * time.Second),
|
||||
Secure: r.URL.Scheme == "https",
|
||||
Domain: r.URL.Hostname(),
|
||||
Path: "/social",
|
||||
}
|
||||
|
||||
if value == "" {
|
||||
cookie.Expires = time.Unix(0, 0)
|
||||
} else {
|
||||
cookie.Value = value
|
||||
}
|
||||
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
+2
-2
@@ -38,7 +38,7 @@ func (ctrl *User) Create(ctx context.Context, r *request.UserCreate) (interface{
|
||||
Email: r.Email,
|
||||
Name: r.Name,
|
||||
Handle: r.Handle,
|
||||
Kind: r.Kind,
|
||||
Kind: types.UserKind(r.Kind),
|
||||
}
|
||||
|
||||
return ctrl.user.With(ctx).Create(user)
|
||||
@@ -50,7 +50,7 @@ func (ctrl *User) Update(ctx context.Context, r *request.UserUpdate) (interface{
|
||||
Email: r.Email,
|
||||
Name: r.Name,
|
||||
Handle: r.Handle,
|
||||
Kind: r.Kind,
|
||||
Kind: types.UserKind(r.Kind),
|
||||
}
|
||||
|
||||
return ctrl.user.With(ctx).Update(user)
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ func MountRoutes(ctx context.Context, r chi.Router) {
|
||||
// Only protect application routes with JWT
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtVerifier, jwtAuthenticator)
|
||||
mountRoutes(r, flags.http, rest.MountRoutes(flags.oidc, flags.social, jwtEncoder))
|
||||
mountRoutes(r, flags.http, rest.MountRoutes(jwtEncoder))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+10
-9
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/crusttech/crust/internal/metrics"
|
||||
"github.com/crusttech/crust/internal/settings"
|
||||
migrate "github.com/crusttech/crust/system/db"
|
||||
"github.com/crusttech/crust/system/internal/auth/external"
|
||||
"github.com/crusttech/crust/system/internal/repository"
|
||||
"github.com/crusttech/crust/system/service"
|
||||
)
|
||||
@@ -48,13 +49,6 @@ func Init(ctx context.Context) error {
|
||||
|
||||
// Load settings from the database,
|
||||
// for now, only at start-up time.
|
||||
ctx := context.Background()
|
||||
// settingsRepository := internalRepository.NewSettings(repository.DB(ctx), "sys_settings")
|
||||
// if ss, err := settingsRepository.Find(types.SettingsFilter{}); err != nil {
|
||||
// panic(err)
|
||||
// } else {
|
||||
// spew.Dump(ss.KV())
|
||||
// }
|
||||
settingService := settings.NewService(settings.NewRepository(repository.DB(ctx), "sys_settings"))
|
||||
|
||||
// configure resputil options
|
||||
@@ -66,8 +60,15 @@ func Init(ctx context.Context) error {
|
||||
},
|
||||
})
|
||||
|
||||
// Don't change this to init(), it needs Database
|
||||
return service.Init()
|
||||
// Don't change this, it needs database connection
|
||||
if err := service.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Setup goth/social authentication
|
||||
external.Init(settingService.With(ctx))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitDatabase(ctx context.Context) error {
|
||||
|
||||
+11
-23
@@ -8,30 +8,18 @@ import (
|
||||
|
||||
type (
|
||||
Credentials struct {
|
||||
ID uint64 `json:"credentialsID,string" db:"id"`
|
||||
OwnerID uint64 `json:"ownerID,string" db:"rel_owner"`
|
||||
Label string `json:"label" db:"label"`
|
||||
Kind CredentialsKind `json:"kind" db:"kind"`
|
||||
Credentials string `json:"-" db:"credentials"`
|
||||
Meta types.JSONText `json:"-" db:"meta"`
|
||||
ExpiresAt *time.Time `json:"expiresAt,omitempty" db:"expires_at"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"`
|
||||
DeletedAt *time.Time `json:"deletedAt,omitempty" db:"deleted_at"`
|
||||
ID uint64 `json:"credentialsID,string" db:"id"`
|
||||
OwnerID uint64 `json:"ownerID,string" db:"rel_owner"`
|
||||
Label string `json:"label" db:"label"`
|
||||
Kind string `json:"kind" db:"kind"`
|
||||
Credentials string `json:"-" db:"credentials"`
|
||||
Meta types.JSONText `json:"-" db:"meta"`
|
||||
LastUsedAt *time.Time `json:"lastUsedAt,omitempty" db:"last_used_at"`
|
||||
ExpiresAt *time.Time `json:"expiresAt,omitempty" db:"expires_at"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updatedAt,omitempty" db:"updated_at"`
|
||||
DeletedAt *time.Time `json:"deletedAt,omitempty" db:"deleted_at"`
|
||||
}
|
||||
|
||||
CredentialsKind string
|
||||
)
|
||||
|
||||
const (
|
||||
// Use as a password for users or as API secret for bots (and credentials-id as a key) as a value for "credentials"
|
||||
CredentialsKindHash CredentialsKind = "hash"
|
||||
|
||||
// Identity (profile-id) stored under "credentials"
|
||||
CredentialsKindFacebook CredentialsKind = "facebook"
|
||||
CredentialsKindGPlus CredentialsKind = "gplus"
|
||||
CredentialsKindGitHub CredentialsKind = "github"
|
||||
CredentialsKindLinkedin CredentialsKind = "linkedin"
|
||||
// CredentialsKindSatosa CredentialsKind = "satosa"
|
||||
)
|
||||
|
||||
func (u *Credentials) Valid() bool {
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/crusttech/crust/internal/rules"
|
||||
)
|
||||
@@ -19,7 +18,6 @@ type (
|
||||
Name string `json:"name" db:"name"`
|
||||
Handle string `json:"handle" db:"handle"`
|
||||
Kind UserKind `json:"kind" db:"kind"`
|
||||
SatosaID string `json:"-" db:"satosa_id"`
|
||||
|
||||
Meta *UserMeta `json:"meta" db:"meta"`
|
||||
|
||||
@@ -27,8 +25,6 @@ type (
|
||||
RelatedUserID uint64 `json:"relatedUserID,string" db:"rel_user_id"`
|
||||
User *User `json:"user" db:"-"`
|
||||
|
||||
Password []byte `json:"-" db:"password"`
|
||||
|
||||
CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updatedAt,omitempty" db:"updated_at"`
|
||||
SuspendedAt *time.Time `json:"suspendedAt,omitempty" db:"suspended_at"`
|
||||
@@ -64,20 +60,6 @@ func (u *User) Identity() uint64 {
|
||||
return u.ID
|
||||
}
|
||||
|
||||
func (u *User) ValidatePassword(password string) bool {
|
||||
return bcrypt.CompareHashAndPassword(u.Password, []byte(password)) == nil
|
||||
}
|
||||
|
||||
func (u *User) GeneratePassword(password string) error {
|
||||
pwd, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.Password = pwd
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resource returns a resource ID for this type
|
||||
func (u *User) PermissionResource() rules.Resource {
|
||||
return UserPermissionResource.AppendID(u.ID)
|
||||
|
||||
+412
@@ -0,0 +1,412 @@
|
||||
package openidConnect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/markbates/goth"
|
||||
"golang.org/x/oauth2"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// Standard Claims http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
||||
// fixed, cannot be changed
|
||||
subjectClaim = "sub"
|
||||
expiryClaim = "exp"
|
||||
audienceClaim = "aud"
|
||||
issuerClaim = "iss"
|
||||
|
||||
PreferredUsernameClaim = "preferred_username"
|
||||
EmailClaim = "email"
|
||||
NameClaim = "name"
|
||||
NicknameClaim = "nickname"
|
||||
PictureClaim = "picture"
|
||||
GivenNameClaim = "given_name"
|
||||
FamilyNameClaim = "family_name"
|
||||
AddressClaim = "address"
|
||||
|
||||
// Unused but available to set in Provider claims
|
||||
MiddleNameClaim = "middle_name"
|
||||
ProfileClaim = "profile"
|
||||
WebsiteClaim = "website"
|
||||
EmailVerifiedClaim = "email_verified"
|
||||
GenderClaim = "gender"
|
||||
BirthdateClaim = "birthdate"
|
||||
ZoneinfoClaim = "zoneinfo"
|
||||
LocaleClaim = "locale"
|
||||
PhoneNumberClaim = "phone_number"
|
||||
PhoneNumberVerifiedClaim = "phone_number_verified"
|
||||
UpdatedAtClaim = "updated_at"
|
||||
|
||||
clockSkew = 10 * time.Second
|
||||
)
|
||||
|
||||
// Provider is the implementation of `goth.Provider` for accessing OpenID Connect provider
|
||||
type Provider struct {
|
||||
ClientKey string
|
||||
Secret string
|
||||
CallbackURL string
|
||||
HTTPClient *http.Client
|
||||
config *oauth2.Config
|
||||
openIDConfig *OpenIDConfig
|
||||
providerName string
|
||||
|
||||
UserIdClaims []string
|
||||
NameClaims []string
|
||||
NickNameClaims []string
|
||||
EmailClaims []string
|
||||
AvatarURLClaims []string
|
||||
FirstNameClaims []string
|
||||
LastNameClaims []string
|
||||
LocationClaims []string
|
||||
|
||||
SkipUserInfoRequest bool
|
||||
}
|
||||
|
||||
type OpenIDConfig struct {
|
||||
AuthEndpoint string `json:"authorization_endpoint"`
|
||||
TokenEndpoint string `json:"token_endpoint"`
|
||||
UserInfoEndpoint string `json:"userinfo_endpoint"`
|
||||
Issuer string `json:"issuer"`
|
||||
}
|
||||
|
||||
// New creates a new OpenID Connect provider, and sets up important connection details.
|
||||
// You should always call `openidConnect.New` to get a new Provider. Never try to create
|
||||
// one manually.
|
||||
// New returns an implementation of an OpenID Connect Authorization Code Flow
|
||||
// See http://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth
|
||||
// ID Token decryption is not (yet) supported
|
||||
// UserInfo decryption is not (yet) supported
|
||||
func New(clientKey, secret, callbackURL, openIDAutoDiscoveryURL string, scopes ...string) (*Provider, error) {
|
||||
p := &Provider{
|
||||
ClientKey: clientKey,
|
||||
Secret: secret,
|
||||
CallbackURL: callbackURL,
|
||||
|
||||
UserIdClaims: []string{subjectClaim},
|
||||
NameClaims: []string{NameClaim},
|
||||
NickNameClaims: []string{NicknameClaim, PreferredUsernameClaim},
|
||||
EmailClaims: []string{EmailClaim},
|
||||
AvatarURLClaims: []string{PictureClaim},
|
||||
FirstNameClaims: []string{GivenNameClaim},
|
||||
LastNameClaims: []string{FamilyNameClaim},
|
||||
LocationClaims: []string{AddressClaim},
|
||||
|
||||
providerName: "openid-connect",
|
||||
}
|
||||
|
||||
openIDConfig, err := getOpenIDConfig(p, openIDAutoDiscoveryURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.openIDConfig = openIDConfig
|
||||
|
||||
p.config = newConfig(p, scopes, openIDConfig)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// 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 openidConnect package.
|
||||
func (p *Provider) Debug(debug bool) {}
|
||||
|
||||
// BeginAuth asks the OpenID Connect provider 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 use the the id_token and access requested information about the user.
|
||||
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
|
||||
sess := session.(*Session)
|
||||
|
||||
expiresAt := sess.ExpiresAt
|
||||
|
||||
if sess.IDToken == "" {
|
||||
return goth.User{}, fmt.Errorf("%s cannot get user information without id_token", p.providerName)
|
||||
}
|
||||
|
||||
// decode returned id token to get expiry
|
||||
claims, err := decodeJWT(sess.IDToken)
|
||||
|
||||
if err != nil {
|
||||
return goth.User{}, fmt.Errorf("oauth2: error decoding JWT token: %v", err)
|
||||
}
|
||||
|
||||
expiry, err := p.validateClaims(claims)
|
||||
if err != nil {
|
||||
return goth.User{}, fmt.Errorf("oauth2: error validating JWT token: %v", err)
|
||||
}
|
||||
|
||||
if expiry.Before(expiresAt) {
|
||||
expiresAt = expiry
|
||||
}
|
||||
|
||||
if err := p.getUserInfo(sess.AccessToken, claims); err != nil {
|
||||
return goth.User{}, err
|
||||
}
|
||||
|
||||
user := goth.User{
|
||||
AccessToken: sess.AccessToken,
|
||||
Provider: p.Name(),
|
||||
RefreshToken: sess.RefreshToken,
|
||||
ExpiresAt: expiresAt,
|
||||
RawData: claims,
|
||||
}
|
||||
|
||||
p.userFromClaims(claims, &user)
|
||||
return user, err
|
||||
}
|
||||
|
||||
//RefreshTokenAvailable refresh token is provided by auth provider or not
|
||||
func (p *Provider) RefreshTokenAvailable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
//RefreshToken get new access token based on the refresh token
|
||||
func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
|
||||
token := &oauth2.Token{RefreshToken: refreshToken}
|
||||
ts := p.config.TokenSource(oauth2.NoContext, token)
|
||||
newToken, err := ts.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newToken, err
|
||||
}
|
||||
|
||||
// validate according to standard, returns expiry
|
||||
// http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
||||
func (p *Provider) validateClaims(claims map[string]interface{}) (time.Time, error) {
|
||||
audience := getClaimValue(claims, []string{audienceClaim})
|
||||
if audience != p.ClientKey {
|
||||
found := false
|
||||
audiences := getClaimValues(claims, []string{audienceClaim})
|
||||
for _, aud := range audiences {
|
||||
if aud == p.ClientKey {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return time.Time{}, errors.New("audience in token does not match client key")
|
||||
}
|
||||
}
|
||||
|
||||
issuer := getClaimValue(claims, []string{issuerClaim})
|
||||
if issuer != p.openIDConfig.Issuer {
|
||||
return time.Time{}, errors.New("issuer in token does not match issuer in OpenIDConfig discovery")
|
||||
}
|
||||
|
||||
// expiry is required for JWT, not for UserInfoResponse
|
||||
// is actually a int64, so force it in to that type
|
||||
expiryClaim := int64(claims[expiryClaim].(float64))
|
||||
expiry := time.Unix(expiryClaim, 0)
|
||||
if expiry.Add(clockSkew).Before(time.Now()) {
|
||||
return time.Time{}, errors.New("user info JWT token is expired")
|
||||
}
|
||||
return expiry, nil
|
||||
}
|
||||
|
||||
func (p *Provider) userFromClaims(claims map[string]interface{}, user *goth.User) {
|
||||
// required
|
||||
user.UserID = getClaimValue(claims, p.UserIdClaims)
|
||||
|
||||
user.Name = getClaimValue(claims, p.NameClaims)
|
||||
user.NickName = getClaimValue(claims, p.NickNameClaims)
|
||||
user.Email = getClaimValue(claims, p.EmailClaims)
|
||||
user.AvatarURL = getClaimValue(claims, p.AvatarURLClaims)
|
||||
user.FirstName = getClaimValue(claims, p.FirstNameClaims)
|
||||
user.LastName = getClaimValue(claims, p.LastNameClaims)
|
||||
user.Location = getClaimValue(claims, p.LocationClaims)
|
||||
}
|
||||
|
||||
func (p *Provider) getUserInfo(accessToken string, claims map[string]interface{}) error {
|
||||
// skip if there is no UserInfoEndpoint or is explicitly disabled
|
||||
if p.openIDConfig.UserInfoEndpoint == "" || p.SkipUserInfoRequest {
|
||||
return nil
|
||||
}
|
||||
|
||||
userInfoClaims, err := p.fetchUserInfo(p.openIDConfig.UserInfoEndpoint, accessToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The sub (subject) Claim MUST always be returned in the UserInfo Response.
|
||||
// http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
|
||||
userInfoSubject := getClaimValue(userInfoClaims, []string{subjectClaim})
|
||||
if userInfoSubject == "" {
|
||||
return fmt.Errorf("userinfo response did not contain a 'sub' claim: %#v", userInfoClaims)
|
||||
}
|
||||
|
||||
// The sub Claim in the UserInfo Response MUST be verified to exactly match the sub Claim in the ID Token;
|
||||
// if they do not match, the UserInfo Response values MUST NOT be used.
|
||||
// http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
|
||||
subject := getClaimValue(claims, []string{subjectClaim})
|
||||
if userInfoSubject != subject {
|
||||
return fmt.Errorf("userinfo 'sub' claim (%s) did not match id_token 'sub' claim (%s)", userInfoSubject, subject)
|
||||
}
|
||||
|
||||
// Merge in userinfo claims in case id_token claims contained some that userinfo did not
|
||||
for k, v := range userInfoClaims {
|
||||
claims[k] = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetch and decode JSON from the given UserInfo URL
|
||||
func (p *Provider) fetchUserInfo(url, accessToken string) (map[string]interface{}, error) {
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||
|
||||
resp, err := p.Client().Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Non-200 response from UserInfo: %d, WWW-Authenticate=%s", resp.StatusCode, resp.Header.Get("WWW-Authenticate"))
|
||||
}
|
||||
|
||||
// The UserInfo Claims MUST be returned as the members of a JSON object
|
||||
// http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return unMarshal(data)
|
||||
}
|
||||
|
||||
func getOpenIDConfig(p *Provider, openIDAutoDiscoveryURL string) (*OpenIDConfig, error) {
|
||||
res, err := p.Client().Get(openIDAutoDiscoveryURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
openIDConfig := &OpenIDConfig{}
|
||||
err = json.Unmarshal(body, openIDConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return openIDConfig, nil
|
||||
}
|
||||
|
||||
func newConfig(provider *Provider, scopes []string, openIDConfig *OpenIDConfig) *oauth2.Config {
|
||||
c := &oauth2.Config{
|
||||
ClientID: provider.ClientKey,
|
||||
ClientSecret: provider.Secret,
|
||||
RedirectURL: provider.CallbackURL,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: openIDConfig.AuthEndpoint,
|
||||
TokenURL: openIDConfig.TokenEndpoint,
|
||||
},
|
||||
Scopes: []string{},
|
||||
}
|
||||
|
||||
if len(scopes) > 0 {
|
||||
foundOpenIDScope := false
|
||||
|
||||
for _, scope := range scopes {
|
||||
if scope == "openid" {
|
||||
foundOpenIDScope = true
|
||||
}
|
||||
c.Scopes = append(c.Scopes, scope)
|
||||
}
|
||||
|
||||
if !foundOpenIDScope {
|
||||
c.Scopes = append(c.Scopes, "openid")
|
||||
}
|
||||
} else {
|
||||
c.Scopes = []string{"openid"}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func getClaimValue(data map[string]interface{}, claims []string) string {
|
||||
for _, claim := range claims {
|
||||
if value, ok := data[claim]; ok {
|
||||
if stringValue, ok := value.(string); ok && len(stringValue) > 0 {
|
||||
return stringValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func getClaimValues(data map[string]interface{}, claims []string) []string {
|
||||
var result []string
|
||||
|
||||
for _, claim := range claims {
|
||||
if value, ok := data[claim]; ok {
|
||||
if stringValues, ok := value.([]interface{}); ok {
|
||||
for _, stringValue := range stringValues {
|
||||
if s, ok := stringValue.(string); ok && len(s) > 0 {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// decodeJWT decodes a JSON Web Token into a simple map
|
||||
// http://openid.net/specs/draft-jones-json-web-token-07.html
|
||||
func decodeJWT(jwt string) (map[string]interface{}, error) {
|
||||
jwtParts := strings.Split(jwt, ".")
|
||||
if len(jwtParts) != 3 {
|
||||
return nil, errors.New("jws: invalid token received, not all parts available")
|
||||
}
|
||||
|
||||
// Re-pad, if needed
|
||||
encodedPayload := jwtParts[1]
|
||||
if l := len(encodedPayload) % 4; l != 0 {
|
||||
encodedPayload += strings.Repeat("=", 4-l)
|
||||
}
|
||||
|
||||
decodedPayload, err := base64.StdEncoding.DecodeString(encodedPayload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return unMarshal(decodedPayload)
|
||||
}
|
||||
|
||||
func unMarshal(payload []byte) (map[string]interface{}, error) {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
return data, json.NewDecoder(bytes.NewBuffer(payload)).Decode(&data)
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package openidConnect
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/markbates/goth"
|
||||
"golang.org/x/oauth2"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Session stores data during the auth process with the OpenID Connect provider.
|
||||
type Session struct {
|
||||
AuthURL string
|
||||
AccessToken string
|
||||
RefreshToken string
|
||||
ExpiresAt time.Time
|
||||
IDToken string
|
||||
}
|
||||
|
||||
// GetAuthURL will return the URL set by calling the `BeginAuth` function on the OpenID Connect provider.
|
||||
func (s Session) GetAuthURL() (string, error) {
|
||||
if s.AuthURL == "" {
|
||||
return "", errors.New("an AuthURL has not be set")
|
||||
}
|
||||
return s.AuthURL, nil
|
||||
}
|
||||
|
||||
// Authorize the session with the OpenID Connect provider 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(oauth2.NoContext, params.Get("code"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !token.Valid() {
|
||||
return "", errors.New("Invalid token received from provider")
|
||||
}
|
||||
|
||||
s.AccessToken = token.AccessToken
|
||||
s.RefreshToken = token.RefreshToken
|
||||
s.ExpiresAt = token.Expiry
|
||||
s.IDToken = token.Extra("id_token").(string)
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user