From 532e28bf6a33c7e956f6e71cf2cb9bc4053bd8fe Mon Sep 17 00:00:00 2001 From: Jo Vandeginste Date: Fri, 6 Sep 2019 15:17:41 +0200 Subject: [PATCH 1/2] Yarn complains about engines Signed-off-by: Jo Vandeginste --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 50cb7a1a771b291d2be5848fd40432807ca45101 Mon Sep 17 00:00:00 2001 From: Jo Vandeginste Date: Fri, 6 Sep 2019 15:18:29 +0200 Subject: [PATCH 2/2] Add support for generic OIDC with auto-discover Eg. when using Keycloak Signed-off-by: Jo Vandeginste --- README.md | 2 +- config/example.yaml | 4 + internal/handlers/auth.go | 6 ++ internal/handlers/auth/generic_oidc.go | 102 ++++++++++++++++++++++++ internal/util/config.go | 3 +- web/public/images/generic_oidc_logo.png | Bin 0 -> 1517 bytes web/src/index.js | 6 ++ 7 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 internal/handlers/auth/generic_oidc.go create mode 100644 web/public/images/generic_oidc_logo.png 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 0000000000000000000000000000000000000000..a3c1a73a61f01e4bd118d205a2ae4ab2f200c24b GIT binary patch literal 1517 zcmeAS@N?(olHy`uVBq!ia0vp^${@_a3?wz#owI=yOS+@4BLl<6e(pbstUx|vage(c z!@6@aFM%9|WRD45bDP46hOx7_4S6Fo+k-*%fHRz`$e` z;1l8sRPcSW+@?*N{&zF{@8|yS!tlQtC?X3KBozS-K?bY09{e>);>RTEB|9!4i(|6a zZQ1>MqUg^_lB>2I!IULcWX;z7zb1+QoFuhu`x&fyA^ycGH6#Rqv4iZ+^_zFznIG|O zvOGj^^rXg`5xkN|6QLN6I;TY44uojf>=cx7q|#kE;;|&v4F)W>fX{$8BTf4&5O6sT(;w< zmBO0TiQH3uuM(Q^t67Kd$%;%1j~5%het7cxl{v59@@IcPZw=*Q*S+-X-70ThL!VT8 zjvN<}TA;HTlf2zs_S#;(R0-s87I;J!18EO1b~~AE2UM`j)5S5w;&gI?0=Gd?L6PC3 z@PlU$pFZC2&%3x`!GsTHf`77Hj&93UUg8#$l(8}~X;Mnts>F#YNrx6W1bIcdh55;? zadQmxjC2hZ3YDBZYggN{Yh}5zu^XgpYn$J;xgAqm=Hrw0Y?*+l*tD=~w;LHEc?BeH z-MDqr+M;Rh!zVc$Lbu*Mdi9Ji?D4~wPkF^Ye*5_KGrOjtAyZ*3Zx3@wB(q{(8aEdo zCoi)$cRzzxXAhI%;^d^qGba2wCo+EmN0{)mBL}9;VM&LDoa<+~O_-%-x+iPWti~*^s1K_$S=o!C`Gn<6ecnD) zIA8w6!0=(%<(sTrE8fYuPq<&O#Ch4heRUH`1BE9&o_Rw($$z!aN7QECYjS`-7Nf!CR`p=u|CnjVMV;EJ?LW zE=mPb3`PcqmbwN&WENs*Y-M0-WoWK#U}R-rV51Qkh@v4kKP5A*5~u;B+CtaRFvP&X z%GA=z2&!S Login with Okta + {info.providers.includes("generic_oidc") &&
} + } + {info.providers.includes("generic_oidc") &&
+
} {info.providers.includes("microsoft") &&