diff --git a/Makefile b/Makefile index 99f8b94..1515ebe 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ runUnitTests: go test -v ./... buildNodeFrontend: - cd web && yarn install + cd web && yarn install --ignore-engines cd web && yarn build cd web && rm build/static/**/*.map diff --git a/README.md b/README.md index 6155860..114d765 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ - Expirable Links - URL deletion - Multiple authorization strategies: - - Local authorization via OAuth 2.0 (Google, GitHub, Microsoft, and Okta) + - Local authorization via OAuth 2.0 (Google, GitHub, Microsoft, and Okta, or generic OIDC like Keycloak) - Proxy authorization for running behind e.g. [Google IAP](https://cloud.google.com/iap/) - Easy [ShareX](https://github.com/ShareX/ShareX) integration - Dockerizable diff --git a/config/example.yaml b/config/example.yaml index 70da763..68715a6 100644 --- a/config/example.yaml +++ b/config/example.yaml @@ -22,6 +22,10 @@ Okta: # only relevant when using the oauth authbackend ClientID: replace me ClientSecret: 'replace me' EndpointURL: # (MANDATORY) Issuer URL from the OAuth API => Authorization Servers in Okta +GenericOIDC: # only relevant when using the oauth authbackend + ClientID: replace me + ClientSecret: 'replace me' + EndpointURL: # (MANDATORY) Base URL, which will be auto-discovered with '.well-known/openid-configuration' Proxy: # only relevant when using the proxy authbackend RequireUserHeader: false # If true, will reject connections that do not have the UserHeader set UserHeader: "X-Goog-Authenticated-User-ID" # pull the unique user ID from this header diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index d5c34a4..1030201 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -47,6 +47,12 @@ func (h *Handler) initOAuth() { h.providers = append(h.providers, "okta") } + genericOIDC := util.GetConfig().GenericOIDC + if genericOIDC.Enabled() { + auth.WithAdapterWrapper(auth.NewGenericOIDCAdapter(genericOIDC.ClientID, genericOIDC.ClientSecret, genericOIDC.EndpointURL), h.engine.Group("/api/v1/auth/generic_oidc")) + h.providers = append(h.providers, "generic_oidc") + } + h.engine.POST("/api/v1/auth/check", h.handleAuthCheck) } diff --git a/internal/handlers/auth/generic_oidc.go b/internal/handlers/auth/generic_oidc.go new file mode 100644 index 0000000..30b0e59 --- /dev/null +++ b/internal/handlers/auth/generic_oidc.go @@ -0,0 +1,102 @@ +package auth + +import ( + "context" + "strings" + + "github.com/mxschmitt/golang-url-shortener/internal/util" + "github.com/sirupsen/logrus" + + oidc "github.com/coreos/go-oidc" + "github.com/pkg/errors" + "golang.org/x/oauth2" +) + +type genericOIDCAdapter struct { + config *oauth2.Config + oidc *oidc.Config + provider *oidc.Provider +} + +type claims struct { + PreferredUsername string `json:"sub"` + Name string `json:"name"` + GivenName string `json:"given_name"` + FamilyName string `json:"family_name"` + ACR string `json:"acr"` +} + +// NewGenericOIDCAdapter creates an oAuth adapter out of the credentials and the baseURL +func NewGenericOIDCAdapter(clientID, clientSecret, endpointURL string) Adapter { + endpointURL = strings.TrimSuffix(endpointURL, "/") + + if endpointURL == "" { + logrus.Error("Configure GenericOIDC Endpoint") + } + + ctx := context.Background() + provider, err := oidc.NewProvider(ctx, endpointURL) + if err != nil { + logrus.Error("Configure GenericOIDC Endpoint: " + err.Error()) + } + + redirectURL := util.GetConfig().BaseURL + "/api/v1/auth/generic_oidc/callback" + // Configure an OpenID Connect aware OAuth client. + return &genericOIDCAdapter{ + config: &oauth2.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + RedirectURL: redirectURL, + // Discovery returns the OAuth endpoints. + Endpoint: provider.Endpoint(), + // "openid" is a required scope for OpenID Connect flows. + Scopes: []string{ + "profile", + "openid", + "offline_access", + }, + }, + oidc: &oidc.Config{ + ClientID: clientID, + }, + provider: provider, + } +} + +func (a *genericOIDCAdapter) GetRedirectURL(state string) string { + return a.config.AuthCodeURL(state) +} + +func (a *genericOIDCAdapter) GetUserData(state, code string) (*user, error) { + + logrus.Debugf("Getting User Data with state: %s, and code: %s", state, code) + oAuthToken, err := a.config.Exchange(context.Background(), code) + if err != nil { + return nil, errors.Wrap(err, "could not exchange code") + } + + rawIDToken, ok := oAuthToken.Extra("id_token").(string) + if !ok { + return nil, errors.Wrap(err, "No id_token field in oauth2 token.") + } + + idToken, err := a.provider.Verifier(a.oidc).Verify(context.Background(), rawIDToken) + if err != nil { + return nil, errors.Wrap(err, "Something went wrong verifying the token: "+err.Error()) + } + + var oUser claims + if err = idToken.Claims(&oUser); err != nil { + return nil, errors.Wrap(err, "Something went wrong verifying the token: "+err.Error()) + } + + return &user{ + ID: string(oUser.PreferredUsername), + Name: oUser.Name, + Picture: util.GetConfig().BaseURL + "/images/generic_oidc_logo.png", // Default GenericOIDC Avatar + }, nil +} + +func (a *genericOIDCAdapter) GetOAuthProviderName() string { + return "generic_oidc" +} diff --git a/internal/util/config.go b/internal/util/config.go index cf4677d..da9d8c8 100644 --- a/internal/util/config.go +++ b/internal/util/config.go @@ -29,6 +29,7 @@ type Configuration struct { GitHub oAuthConf `yaml:"GitHub" env:"GITHUB"` Microsoft oAuthConf `yaml:"Microsoft" env:"MICROSOFT"` Okta oAuthConf `yaml:"Okta" env:"OKTA"` + GenericOIDC oAuthConf `yaml:"GenericOIDC" env:"GENERIC_OIDC"` Proxy proxyAuthConf `yaml:"Proxy" env:"PROXY"` Redis redisConf `yaml:"Redis" env:"REDIS"` } @@ -47,7 +48,7 @@ type redisConf struct { type oAuthConf struct { ClientID string `yaml:"ClientID" env:"CLIENT_ID"` ClientSecret string `yaml:"ClientSecret" env:"CLIENT_SECRET"` - EndpointURL string `yaml:"EndpointURL" env:"ENDPOINT_URL"` // Optional for GitHub, mandatory for Okta + EndpointURL string `yaml:"EndpointURL" env:"ENDPOINT_URL"` // Optional for GitHub, mandatory for Okta and GenericOIDC } type proxyAuthConf struct { diff --git a/web/public/images/generic_oidc_logo.png b/web/public/images/generic_oidc_logo.png new file mode 100644 index 0000000..a3c1a73 Binary files /dev/null and b/web/public/images/generic_oidc_logo.png differ diff --git a/web/src/index.js b/web/src/index.js index 0543a4d..ec2794c 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -137,6 +137,12 @@ export default class BaseComponent extends Component { + {info.providers.includes("generic_oidc") &&
} + } + {info.providers.includes("generic_oidc") &&