diff --git a/README.md b/README.md index d0dcfdf..6155860 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 and Microsoft) + - Local authorization via OAuth 2.0 (Google, GitHub, Microsoft, and Okta) - 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 7d7caef..70da763 100644 --- a/config/example.yaml +++ b/config/example.yaml @@ -1,5 +1,5 @@ ListenAddr: ':8080' # Consists of 'IP:Port', e.g. ':8080' listens on any IP and on Port 8080 -BaseURL: 'http://localhost:3000' # Origin URL, required for the authentication via OAuth callback +BaseURL: 'http://localhost:8080' # Origin URL, required for the authentication via OAuth callback DisplayURL: '' # (OPTIONAL) Display URL, how the apication will present itself in the UI - if not set, defaults to BaseURL Backend: boltdb # Can be 'boltdb' or 'redis' DataDir: ./data # Contains: the database and the private key @@ -10,14 +10,18 @@ ShortedIDLength: 10 # Length of the random generated ID which is used for new sh AuthBackend: oauth # Can be 'oauth' or 'proxy' Google: # only relevant when using the oauth authbackend ClientID: replace me - ClientSecret: replace me + ClientSecret: 'replace me' GitHub: # only relevant when using the oauth authbackend ClientID: replace me - ClientSecret: replace me + ClientSecret: 'replace me' EndpointURL: # (OPTIONAL) URL for custom endpoint (currently only for github); e.g. 'https://github.mydomain.com' Microsoft: # only relevant when using the oauth authbackend ClientID: replace me ClientSecret: 'replace me' +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 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 839dc88..d5c34a4 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -41,6 +41,11 @@ func (h *Handler) initOAuth() { auth.WithAdapterWrapper(auth.NewMicrosoftAdapter(microsoft.ClientID, microsoft.ClientSecret), h.engine.Group("/api/v1/auth/microsoft")) h.providers = append(h.providers, "microsoft") } + okta := util.GetConfig().Okta + if okta.Enabled() { + auth.WithAdapterWrapper(auth.NewOktaAdapter(okta.ClientID, okta.ClientSecret, okta.EndpointURL), h.engine.Group("/api/v1/auth/okta")) + h.providers = append(h.providers, "okta") + } h.engine.POST("/api/v1/auth/check", h.handleAuthCheck) } diff --git a/internal/handlers/auth/okta.go b/internal/handlers/auth/okta.go new file mode 100644 index 0000000..6a88fd5 --- /dev/null +++ b/internal/handlers/auth/okta.go @@ -0,0 +1,84 @@ +package auth + +import ( + "context" + "encoding/json" + "net/url" + "strings" + + "github.com/mxschmitt/golang-url-shortener/internal/util" + "github.com/sirupsen/logrus" + + "github.com/pkg/errors" + "golang.org/x/oauth2" +) + +type oktaAdapter struct { + config *oauth2.Config +} + +// NewOktaAdapter creates an oAuth adapter out of the credentials and the baseURL +func NewOktaAdapter(clientID, clientSecret, endpointURL string) Adapter { + + if endpointURL == "" { + logrus.Error("Configure Okta Endpoint") + } + + return &oktaAdapter{&oauth2.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + RedirectURL: util.GetConfig().BaseURL + "/api/v1/auth/okta/callback", + Scopes: []string{ + "profile", + "openid", + "offline_access", + }, + Endpoint: oauth2.Endpoint{ + AuthURL: endpointURL + "/v1/authorize", + TokenURL: endpointURL + "/v1/token", + }, + }} +} + +func (a *oktaAdapter) GetRedirectURL(state string) string { + return a.config.AuthCodeURL(state) +} + +func (a *oktaAdapter) 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") + } + if util.GetConfig().Okta.EndpointURL == "" { + logrus.Error("Okta EndpointURL is Empty") + } + oktaUrl, err := url.Parse(util.GetConfig().Okta.EndpointURL) + if err != nil { + return nil, errors.Wrap(err, "could not parse Okta EndpointURL") + } + oktaBaseURL := strings.Replace(oktaUrl.String(), oktaUrl.RequestURI(), "", 1) + oAuthUserInfoReq, err := a.config.Client(context.Background(), oAuthToken).Get(oktaBaseURL + "/api/v1/users/me") + if err != nil { + return nil, errors.Wrap(err, "could not get user data") + } + defer oAuthUserInfoReq.Body.Close() + var oUser struct { + ID int `json:"sub"` + // Custom URL property for user Avatar can go here + Name string `json:"name"` + } + if err = json.NewDecoder(oAuthUserInfoReq.Body).Decode(&oUser); err != nil { + return nil, errors.Wrap(err, "decoding user info failed") + } + return &user{ + ID: string(oUser.ID), + Name: oUser.Name, + Picture: util.GetConfig().BaseURL + "/images/okta_logo.png", // Default Okta Avatar + }, nil +} + +func (a *oktaAdapter) GetOAuthProviderName() string { + return "okta" +} diff --git a/internal/util/config.go b/internal/util/config.go index f014366..cf4677d 100644 --- a/internal/util/config.go +++ b/internal/util/config.go @@ -28,6 +28,7 @@ type Configuration struct { Google oAuthConf `yaml:"Google" env:"GOOGLE"` GitHub oAuthConf `yaml:"GitHub" env:"GITHUB"` Microsoft oAuthConf `yaml:"Microsoft" env:"MICROSOFT"` + Okta oAuthConf `yaml:"Okta" env:"OKTA"` Proxy proxyAuthConf `yaml:"Proxy" env:"PROXY"` Redis redisConf `yaml:"Redis" env:"REDIS"` } @@ -46,7 +47,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 only GitHub + EndpointURL string `yaml:"EndpointURL" env:"ENDPOINT_URL"` // Optional for GitHub, mandatory for Okta } type proxyAuthConf struct { @@ -58,7 +59,7 @@ type proxyAuthConf struct { // Config contains the default values var Config = Configuration{ ListenAddr: ":8080", - BaseURL: "http://localhost:3000", + BaseURL: "http://localhost:8080", DisplayURL: "", DataDir: "data", Backend: "boltdb", diff --git a/web/package.json b/web/package.json index 3005cdd..b3b1872 100644 --- a/web/package.json +++ b/web/package.json @@ -35,6 +35,9 @@ "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, + "resolutions": { + "react-router": "4.3.1" + }, "browserslist": [ ">0.2%", "not dead", diff --git a/web/public/images/okta_logo.png b/web/public/images/okta_logo.png new file mode 100644 index 0000000..c058652 Binary files /dev/null and b/web/public/images/okta_logo.png differ diff --git a/web/src/index.js b/web/src/index.js index 8d65322..0543a4d 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -131,6 +131,12 @@ export default class BaseComponent extends Component { + {info.providers.includes("okta") &&
} + } + {info.providers.includes("okta") &&