+
+
+
+
+
+
+
+
+
+
+
+
+ {{range $idx, $prov := $.LoginProviders}}
+ {{with ne $idx 0}}
+
+ {{end}}
+
{{$prov.Name}}
+ {{end}}
+
+
-
-
-
Go Home
+ {{ if eq .HasError true }}
+
+ {{.Message}}
+
+ {{end}}
+
+
+
+
+
{{template "prt_flashes.html" .}}
diff --git a/cmd/wg-portal/common/config.go b/cmd/wg-portal/common/config.go
index 3b8531a..bd466aa 100644
--- a/cmd/wg-portal/common/config.go
+++ b/cmd/wg-portal/common/config.go
@@ -5,6 +5,51 @@
"github.com/h44z/wg-portal/internal/portal"
)
+type OpenIDConnectProvider struct {
+ // ProviderName is an internal name that is used to distinguish oauth endpoints. It must not contain spaces or special characters.
+ ProviderName string
+
+ // DisplayName is shown to the user on the login page. If it is empty, ProviderName will be displayed.
+ DisplayName string
+
+ BaseUrl string
+
+ // ClientID is the application's ID.
+ ClientID string
+
+ // ClientSecret is the application's secret.
+ ClientSecret string
+
+ Scopes []string
+}
+
+type OAuthProvider struct {
+ // ProviderName is an internal name that is used to distinguish oauth endpoints. It must not contain spaces or special characters.
+ ProviderName string
+
+ // DisplayName is shown to the user on the login page. If it is empty, ProviderName will be displayed.
+ DisplayName string
+
+ BaseUrl string
+
+ // ClientID is the application's ID.
+ ClientID string
+
+ // ClientSecret is the application's secret.
+ ClientSecret string
+
+ AuthURL string
+ TokenURL string
+ UserInfoURL string
+
+ // RedirectURL is the URL to redirect users going through
+ // the OAuth flow, after the resource owner's URLs.
+ RedirectURL string
+
+ // Scope specifies optional requested permissions.
+ Scopes []string
+}
+
type Config struct {
Core struct {
GinDebug bool `yaml:"ginDebug" envconfig:"GIN_DEBUG"`
@@ -28,6 +73,11 @@
LogoUrl string `yaml:"logoUrl" envconfig:"LOGO_URL"`
} `yaml:"core"`
+ Auth struct {
+ OpenIDConnect []OpenIDConnectProvider `yaml:"openIdCconnect"`
+ OAuth []OAuthProvider `yaml:"oauth"`
+ } `yaml:"auth"`
+
Mail portal.MailConfig `yaml:"email"`
Database persistence.DatabaseConfig `yaml:"database"`
}
diff --git a/cmd/wg-portal/main.go b/cmd/wg-portal/main.go
index 1a89e32..9930aff 100644
--- a/cmd/wg-portal/main.go
+++ b/cmd/wg-portal/main.go
@@ -52,6 +52,14 @@
cfg.Core.LogLevel = "trace"
cfg.Core.CompanyName = "Test Company"
cfg.Core.LogoUrl = "/img/header-logo.png"
+
+ cfg.Auth.OpenIDConnect = []common.OpenIDConnectProvider{
+ {
+ ProviderName: "google",
+ DisplayName: "Login withGoogle",
+ BaseUrl: "https://accounts.google.com",
+ },
+ }
// TODO: load config
srv, err := NewServer(cfg)
diff --git a/cmd/wg-portal/ui/handler.go b/cmd/wg-portal/ui/handler.go
index fadf661..15decdb 100644
--- a/cmd/wg-portal/ui/handler.go
+++ b/cmd/wg-portal/ui/handler.go
@@ -1,25 +1,85 @@
package ui
import (
+ "context"
+ "net/url"
+ "path"
+
+ "golang.org/x/oauth2"
+
+ "github.com/coreos/go-oidc"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/h44z/wg-portal/cmd/wg-portal/common"
"github.com/h44z/wg-portal/internal/portal"
+ "github.com/pkg/errors"
"github.com/sirupsen/logrus"
csrf "github.com/utrack/gin-csrf"
)
+type AuthProviderType string
+
+const (
+ AuthProviderTypeOAuth = "oauth"
+ AuthProviderTypeOpenIDConnect = "oidc"
+)
+
type Handler struct {
config *common.Config
- backend portal.Backend
+ backend portal.Backend
+ authProviderNames map[string]AuthProviderType
+ oidcProviders map[string]*oidc.Provider
+ oauthConfigs map[string]oauth2.Config
}
func NewHandler(config *common.Config, backend portal.Backend) (*Handler, error) {
h := &Handler{
- config: config,
- backend: backend,
+ config: config,
+ backend: backend,
+ authProviderNames: make(map[string]AuthProviderType),
+ oidcProviders: make(map[string]*oidc.Provider),
+ oauthConfigs: make(map[string]oauth2.Config),
}
+
+ extUrl, err := url.Parse(config.Core.ExternalUrl)
+ if err != nil {
+ return nil, errors.WithMessage(err, "failed to parse external url")
+ }
+
+ for _, provider := range h.config.Auth.OpenIDConnect {
+ if _, exists := h.authProviderNames[provider.ProviderName]; exists {
+ return nil, errors.Errorf("auth provider with name %s is already registerd", provider.ProviderName)
+ }
+ h.authProviderNames[provider.ProviderName] = AuthProviderTypeOpenIDConnect
+
+ var err error
+ h.oidcProviders[provider.ProviderName], err = oidc.NewProvider(context.Background(), provider.BaseUrl)
+ if err != nil {
+ return nil, errors.WithMessagef(err, "failed to setup oidc provider %s", provider.ProviderName)
+ }
+
+ redirecUrl := *extUrl
+ redirecUrl.Path = path.Join(redirecUrl.Path, "/auth/login/", provider.ProviderName, "/callback")
+ scopes := []string{oidc.ScopeOpenID}
+ scopes = append(scopes, provider.Scopes...)
+ h.oauthConfigs[provider.ProviderName] = oauth2.Config{
+ ClientID: provider.ClientID,
+ ClientSecret: provider.ClientSecret,
+ Endpoint: h.oidcProviders[provider.ProviderName].Endpoint(),
+ RedirectURL: redirecUrl.String(),
+ Scopes: scopes,
+ }
+ }
+ for _, provider := range h.config.Auth.OAuth {
+ if _, exists := h.authProviderNames[provider.ProviderName]; exists {
+ return nil, errors.Errorf("auth provider with name %s is already registerd", provider.ProviderName)
+ }
+ h.authProviderNames[provider.ProviderName] = AuthProviderTypeOAuth
+
+ // TODO
+ }
+
return h, nil
}
@@ -39,7 +99,9 @@
auth := g.Group("/auth")
auth.Use(csrfMiddleware)
auth.GET("/login", h.GetLogin)
- //auth.POST("/login", s.PostLogin)
+ auth.POST("/login", h.PostLogin)
+ auth.GET("/login/:provider", h.GetLoginOauth)
+ auth.GET("/login/:provider/callback", h.GetLoginOauthCallback)
//auth.GET("/logout", s.GetLogout)
// Admin routes
diff --git a/cmd/wg-portal/ui/pages_core.go b/cmd/wg-portal/ui/pages_core.go
index cd4b82c..5b4a8d5 100644
--- a/cmd/wg-portal/ui/pages_core.go
+++ b/cmd/wg-portal/ui/pages_core.go
@@ -1,7 +1,9 @@
package ui
import (
+ "html/template"
"net/http"
+ "strings"
"time"
"github.com/gin-gonic/gin"
@@ -32,6 +34,11 @@
})
}
+type LoginProviderInfo struct {
+ Name template.HTML
+ Url string
+}
+
func (h *Handler) GetLogin(c *gin.Context) {
currentSession := GetSessionData(c)
if currentSession.LoggedIn {
@@ -50,6 +57,58 @@
errMsg = "Login required!"
}
+ authProviders := make([]LoginProviderInfo, 0, len(h.config.Auth.OAuth)+len(h.config.Auth.OpenIDConnect))
+ for _, provider := range h.config.Auth.OpenIDConnect {
+ providerId := strings.ToLower(provider.ProviderName)
+ providerName := provider.DisplayName
+ if providerName == "" {
+ providerName = provider.ProviderName
+ }
+ authProviders = append(authProviders, LoginProviderInfo{
+ Name: template.HTML(providerName),
+ Url: "/auth/login/" + providerId,
+ })
+ }
+ for _, provider := range h.config.Auth.OAuth {
+ providerId := strings.ToLower(provider.ProviderName)
+ providerName := provider.DisplayName
+ if providerName == "" {
+ providerName = provider.ProviderName
+ }
+ authProviders = append(authProviders, LoginProviderInfo{
+ Name: template.HTML(providerName),
+ Url: "/auth/login/" + providerId,
+ })
+ }
+
+ c.HTML(http.StatusOK, "login.html", gin.H{
+ "HasError": authError != "",
+ "Message": errMsg,
+ "DeepLink": deepLink,
+ "Static": h.getStaticData(),
+ "Csrf": csrf.GetToken(c),
+ "LoginProviders": authProviders,
+ })
+}
+
+func (h *Handler) PostLogin(c *gin.Context) {
+ currentSession := GetSessionData(c)
+ if currentSession.LoggedIn {
+ c.Redirect(http.StatusSeeOther, "/") // already logged in
+ }
+
+ deepLink := c.DefaultQuery("dl", "")
+ authError := c.DefaultQuery("err", "")
+ errMsg := "Unknown error occurred, try again!"
+ switch authError {
+ case "missingdata":
+ errMsg = "Invalid login data retrieved, please fill out all fields and try again!"
+ case "authfail":
+ errMsg = "Authentication failed!"
+ case "loginreq":
+ errMsg = "Login required!"
+ }
+
c.HTML(http.StatusOK, "login.html", gin.H{
"HasError": authError != "",
"Message": errMsg,
@@ -58,3 +117,25 @@
"Csrf": csrf.GetToken(c),
})
}
+
+func (h *Handler) GetLoginOauth(c *gin.Context) {
+ currentSession := GetSessionData(c)
+ if currentSession.LoggedIn {
+ c.Redirect(http.StatusSeeOther, "/") // already logged in
+ }
+
+ provider := c.Param("provider")
+ if _, ok := h.authProviderNames[provider]; !ok {
+ c.Redirect(http.StatusSeeOther, "/auth/login?err=invalidprovider")
+ return
+ }
+
+ switch h.authProviderNames[provider] {
+ case AuthProviderTypeOAuth:
+ case AuthProviderTypeOpenIDConnect:
+ }
+}
+
+func (h *Handler) GetLoginOauthCallback(c *gin.Context) {
+ //code := c.PostForm("code")
+}
diff --git a/go.mod b/go.mod
index da42907..82d7416 100644
--- a/go.mod
+++ b/go.mod
@@ -3,11 +3,13 @@
go 1.16
require (
+ github.com/coreos/go-oidc v2.2.1+incompatible
github.com/gin-contrib/sessions v0.0.3
github.com/gin-gonic/gin v1.7.4
github.com/kr/text v0.2.0 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/pkg/errors v0.9.1
+ github.com/pquerna/cachecontrol v0.1.0 // indirect
github.com/sirupsen/logrus v1.4.2
github.com/stretchr/testify v1.7.0
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f
@@ -15,9 +17,11 @@
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca
github.com/vishvananda/netlink v1.1.0
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
+ golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 // indirect
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210506160403-92e472f520a5
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
+ gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
gorm.io/driver/mysql v1.1.2
gorm.io/driver/postgres v1.1.2