diff --git a/cmd/wg-portal/assets/tpl/login.html b/cmd/wg-portal/assets/tpl/login.html index c84e40f..138d80b 100644 --- a/cmd/wg-portal/assets/tpl/login.html +++ b/cmd/wg-portal/assets/tpl/login.html @@ -21,38 +21,60 @@
-
-
Please sign in
-
-
-
- - -
- - - We'll never share your email with anyone else. -
-
- - -
- +
+
+
+
+
Please sign in
+
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+ + +
+
- {{ if eq .HasError true }} - - {{end}} -
- +
+
+ +
+
+ + {{range $idx, $prov := $.LoginProviders}} + {{with ne $idx 0}} +
+ {{end}} + {{$prov.Name}} + {{end}} +
+
-
-
- Go Home + {{ if eq .HasError true }} + + {{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 with
Google", + 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