Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Introduce a ThirdParty GenericOIDC implementation. #1321

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build the hanko binary
FROM --platform=$BUILDPLATFORM golang:1.20 AS builder
FROM --platform=$BUILDPLATFORM golang:1.21 AS builder

ARG TARGETARCH

Expand Down
2 changes: 1 addition & 1 deletion backend/Dockerfile.debug
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build the hanko binary
FROM golang:1.20 AS builder
FROM golang:1.21 AS builder
WORKDIR /workspace

# Get Delve
Expand Down
84 changes: 72 additions & 12 deletions backend/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@ package config
import (
"errors"
"fmt"
"log"
"strings"
"time"

"github.com/fatih/structs"
"github.com/gobwas/glob"
"github.com/kelseyhightower/envconfig"
"github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
zeroLogger "github.com/rs/zerolog/log"
"github.com/teamhanko/hanko/backend/ee/saml/config"
"golang.org/x/exp/slices"
zeroLogger "github.com/rs/zerolog/log"
"log"
"strings"
"time"
)

// Config is the central configuration type
Expand Down Expand Up @@ -91,6 +92,20 @@ func Load(cfgFile *string) (*Config, error) {
return nil, fmt.Errorf("failed to validate config: %s", err)
}

if c.ThirdParty.GenericOIDCProviders != nil {
fixedGenericOIDCProviders := make(map[string]GenericOIDCProvider)
for k, v := range c.ThirdParty.GenericOIDCProviders {
kl := strings.ToLower(k)
v.Slug = kl
fixedGenericOIDCProviders[kl] = v
}
c.ThirdParty.GenericOIDCProviders = fixedGenericOIDCProviders
}
s := structs.New(c.ThirdParty.Providers)
for _, field := range s.Fields() {
v := field.Value().(ThirdPartyProvider)
v.Slug = strings.ToLower(field.Name())
}
return c, nil
}

Expand Down Expand Up @@ -533,11 +548,12 @@ type RedisConfig struct {
}

type ThirdParty struct {
Providers ThirdPartyProviders `yaml:"providers" json:"providers,omitempty" koanf:"providers"`
RedirectURL string `yaml:"redirect_url" json:"redirect_url,omitempty" koanf:"redirect_url" split_words:"true"`
ErrorRedirectURL string `yaml:"error_redirect_url" json:"error_redirect_url,omitempty" koanf:"error_redirect_url" split_words:"true"`
AllowedRedirectURLS []string `yaml:"allowed_redirect_urls" json:"allowed_redirect_urls,omitempty" koanf:"allowed_redirect_urls" split_words:"true"`
AllowedRedirectURLMap map[string]glob.Glob `jsonschema:"-"`
GenericOIDCProviders map[string]GenericOIDCProvider `yaml:"generic_oidc_providers" json:"generic_oidc_providers,omitempty" koanf:"generic_oidc_providers"`
Providers ThirdPartyProviders `yaml:"providers" json:"providers,omitempty" koanf:"providers"`
RedirectURL string `yaml:"redirect_url" json:"redirect_url,omitempty" koanf:"redirect_url" split_words:"true"`
ErrorRedirectURL string `yaml:"error_redirect_url" json:"error_redirect_url,omitempty" koanf:"error_redirect_url" split_words:"true"`
AllowedRedirectURLS []string `yaml:"allowed_redirect_urls" json:"allowed_redirect_urls,omitempty" koanf:"allowed_redirect_urls" split_words:"true"`
AllowedRedirectURLMap map[string]glob.Glob `jsonschema:"-"`
}

func (t *ThirdParty) Validate() error {
Expand Down Expand Up @@ -585,9 +601,39 @@ func (t *ThirdParty) PostProcess() error {
}

type ThirdPartyProvider struct {
Enabled bool `yaml:"enabled" json:"enabled" koanf:"enabled"`
DisplayName string `yaml:"display_name" json:"display_name" koanf:"display_name"`
Enabled bool `yaml:"enabled" json:"enabled" koanf:"enabled"`
ClientID string `yaml:"client_id" json:"client_id" koanf:"client_id" split_words:"true"`
Secret string `yaml:"secret" json:"secret" koanf:"secret"`
Hidden bool `yaml:"hidden" json:"hidden" koanf:"hidden"`
Slug string
}

type GenericOIDCProvider struct {
// DisplayName is the name of the provider that is displayed to the user.
DisplayName string `yaml:"display_name" json:"display_name" koanf:"display_name"`
// Enabled indicates if the provider is enabled.
Enabled bool `yaml:"enabled" json:"enabled" koanf:"enabled"`
// ClientID is the client ID of the provider.
ClientID string `yaml:"client_id" json:"client_id" koanf:"client_id" split_words:"true"`
Secret string `yaml:"secret" json:"secret" koanf:"secret"`
// Secret is the client secret of the provider.
Secret string `yaml:"secret" json:"secret" koanf:"secret"`
// Scopes is a space-separated list of scopes that the provider requires.
Scopes string `yaml:"scopes" json:"scopes" koanf:"scopes"`
// Hidden indicates if the provider should be hidden from the UI. Hidden as an option duing login/signup and later in IDP management.
Hidden bool `yaml:"hidden" json:"hidden" koanf:"hidden"`
// Authority is the OIDC authority URL of the provider.
Authority string `yaml:"authority" json:"authority" koanf:"authority"`
// RequireProviderEmailVerification indicates if the provider should require email verification on their end.
RequireProviderEmailVerification bool `yaml:"require_provider_email_verification" json:"require_provider_email_verification" koanf:"require_provider_email_verification" split_words:"true"`
// ImageRef is a reference to an image that can be used to display the provider's logo.
ImageRef string `yaml:"image_ref" json:"image_ref" koanf:"image_ref"`
// Metadata is a map of arbitrary key-value pairs that can be used to store additional information about the provider.
// TODO: Should be exposed via an admin API for the frontend to consume.
Metadata map[string]string `yaml:"metadata" json:"metadata" koanf:"metadata"`
// Slug is a unique identifier for the provider. It is used to reference the provider in the configuration.
// it is the lowercase version of the third party map key
Slug string
}

func (p *ThirdPartyProvider) Validate() error {
Expand All @@ -601,6 +647,20 @@ func (p *ThirdPartyProvider) Validate() error {
}
return nil
}
func (p *GenericOIDCProvider) Validate() error {
if p.Enabled {
if p.Authority == "" {
return errors.New("missing authority")
}
if p.ClientID == "" {
return errors.New("missing client ID")
}
if p.Secret == "" {
return errors.New("missing client secret")
}
}
return nil
}

type ThirdPartyProviders struct {
Google ThirdPartyProvider `yaml:"google" json:"google,omitempty" koanf:"google"`
Expand Down Expand Up @@ -665,7 +725,7 @@ func (c *Config) arrangeSmtpSettings() {
zeroLogger.Warn().Msg("Both root smtp and passcode.smtp are set. Using smtp settings from root configuration")
return
}

c.Smtp = c.Passcode.Smtp
}
}
Expand Down
35 changes: 31 additions & 4 deletions backend/dto/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,48 @@ func FromConfig(config config.Config) PublicConfig {
return PublicConfig{
Password: config.Password,
Emails: config.Emails,
Providers: GetEnabledProviders(config.ThirdParty.Providers),
Providers: GetEnabledProviders(config.ThirdParty),
Account: config.Account,
UseEnterpriseConnection: UseEnterpriseConnection(&config.Saml),
}
}

func GetEnabledProviders(providers config.ThirdPartyProviders) []string {
func GetEnabledProviders(thirdParty config.ThirdParty) []string {
// TODO: a provider should return an slug and the display name.
// It looks like the name, aka what is displayed is also used to request and auth
providers := thirdParty.Providers
s := structs.New(providers)
var enabledProviders []string
for _, field := range s.Fields() {
v := field.Value().(config.ThirdPartyProvider)
if v.Enabled {
enabledProviders = append(enabledProviders, field.Name())
if v.Enabled && !v.Hidden {
displayName := field.Name()
/*
// fails because the display name is uses as the lookup key.
// need to send the client both.
if v.DisplayName != "" {
displayName = v.DisplayName
}
*/
enabledProviders = append(enabledProviders, displayName)
}
}
if thirdParty.GenericOIDCProviders != nil {
for k, v := range thirdParty.GenericOIDCProviders {
if v.Enabled && !v.Hidden {
displayName := k
/*
// fails because the display name is uses as the lookup key.
// need to send the client both.
if v.DisplayName != "" {
displayName = v.DisplayName
}
*/
enabledProviders = append(enabledProviders, displayName)
}
}
}

return enabledProviders
}

Expand Down
18 changes: 10 additions & 8 deletions backend/ee/saml/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ package saml
import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"

"github.com/gobuffalo/pop/v6"
"github.com/labstack/echo/v4"
saml2 "github.com/russellhaering/gosaml2"
Expand All @@ -16,9 +20,6 @@ import (
"github.com/teamhanko/hanko/backend/session"
"github.com/teamhanko/hanko/backend/thirdparty"
"github.com/teamhanko/hanko/backend/utils"
"net/http"
"net/url"
"strings"
)

type SamlHandler struct {
Expand Down Expand Up @@ -229,11 +230,12 @@ func (handler *SamlHandler) linkAccount(c echo.Context, redirectTo *url.___URL, sta
query.Add(utils.HankoTokenQuery, token.Value)
redirectTo.RawQuery = query.Encode()

cookie := utils.GenerateStateCookie(handler.config, utils.HankoThirdpartyStateCookie, "", utils.CookieOptions{
MaxAge: -1,
Path: "/",
SameSite: http.SameSiteLaxMode,
})
cookie := utils.GenerateStateCookie(handler.config,
utils.HankoThirdpartyStateCookie, "", utils.CookieOptions{
MaxAge: -1,
Path: "/",
SameSite: http.SameSiteLaxMode,
})
c.SetCookie(cookie)

return nil
Expand Down
21 changes: 13 additions & 8 deletions backend/go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
module github.com/teamhanko/hanko/backend

go 1.20
go 1.21

toolchain go1.21.6

require (
github.com/brianvoe/gofakeit/v6 v6.28.0
github.com/coreos/go-oidc/v3 v3.9.0
github.com/fatih/structs v1.1.0
github.com/go-playground/validator/v10 v10.17.0
github.com/go-sql-driver/mysql v1.7.1
Expand Down Expand Up @@ -37,7 +40,7 @@ require (
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.8.4
golang.org/x/crypto v0.18.0
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc
golang.org/x/oauth2 v0.16.0
golang.org/x/text v0.14.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
Expand All @@ -51,7 +54,7 @@ require (
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/beevik/etree v1.1.0 // indirect
Expand All @@ -72,6 +75,7 @@ require (
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.6.1 // indirect
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-webauthn/x v0.1.6 // indirect
Expand All @@ -87,6 +91,7 @@ require (
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-tpm v0.9.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/css v1.0.0 // indirect
Expand All @@ -105,7 +110,7 @@ require (
github.com/jonboulle/clockwork v0.3.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
Expand Down Expand Up @@ -155,13 +160,13 @@ require (
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.opentelemetry.io/otel v1.15.0 // indirect
go.opentelemetry.io/otel/trace v1.15.0 // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.7.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
golang.org/x/tools v0.16.1 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
Expand Down
Loading