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/go.mod b/go.mod index 9268b33..34294b4 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.15 require ( github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef github.com/boltdb/bolt v1.3.1 + github.com/coreos/go-oidc v2.2.1+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/gin-contrib/sessions v0.0.3 github.com/gin-gonic/gin v1.6.3 @@ -15,9 +16,11 @@ require ( github.com/mxschmitt/golang-env-struct v0.0.0-20181017075525-0c54aeca8397 github.com/pborman/uuid v1.2.1 github.com/pkg/errors v0.8.0 + github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac // indirect github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 github.com/sirupsen/logrus v1.4.2 golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be + gopkg.in/square/go-jose.v2 v2.5.1 // indirect gopkg.in/yaml.v2 v2.2.8 ) diff --git a/go.sum b/go.sum index 86d4e0e..60e60bd 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,8 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= +github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= @@ -148,6 +150,8 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac h1:jWKYCNlX4J5s8M0nHYkh7Y7c9gRVDEb3mq51j5J0F5M= +github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac/go.mod h1:hoLfEwdY11HjRfKFH6KqnPsfxlo3BP6bJehpDv8t6sQ= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= @@ -184,6 +188,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= @@ -259,10 +264,13 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index d5c34a4..a354bb9 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -46,6 +46,11 @@ func (h *Handler) initOAuth() { auth.WithAdapterWrapper(auth.NewOktaAdapter(okta.ClientID, okta.ClientSecret, okta.EndpointURL), h.engine.Group("/api/v1/auth/okta")) 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/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") &&
+
} {info.providers.includes("microsoft") &&