From b98777f1d934716aeffc6bb6762d9bd8facebc97 Mon Sep 17 00:00:00 2001 From: willemvd Date: Tue, 21 Feb 2017 08:34:11 +0100 Subject: [PATCH 01/19] add google+ --- models/login_source.go | 3 +- modules/auth/oauth2/oauth2.go | 3 + public/img/{ => auth}/github.png | Bin public/img/auth/google_plus.png | Bin 0 -> 4366 bytes .../markbates/goth/providers/gplus/gplus.go | 195 ++++++++++++++++++ .../markbates/goth/providers/gplus/session.go | 61 ++++++ vendor/vendor.json | 6 + 7 files changed, 267 insertions(+), 1 deletion(-) rename public/img/{ => auth}/github.png (100%) create mode 100644 public/img/auth/google_plus.png create mode 100644 vendor/github.com/markbates/goth/providers/gplus/gplus.go create mode 100644 vendor/github.com/markbates/goth/providers/gplus/session.go diff --git a/models/login_source.go b/models/login_source.go index 8d5d08dea6aa1..00b5d80c9f54c 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -598,7 +598,8 @@ type OAuth2Provider struct { // key is used to map the OAuth2Provider with the goth provider type (also in LoginSource.OAuth2Config.Provider) // value is used to store display data var OAuth2Providers = map[string]OAuth2Provider{ - "github": {Name: "github", DisplayName:"GitHub", Image: "/img/github.png"}, + "github": {Name: "github", DisplayName:"GitHub", Image: "/img/auth/github.png"}, + "gplus": {Name: "gplus", DisplayName:"Google+", Image: "/img/auth/google_plus.png"}, } // ExternalUserLogin attempts a login using external source types. diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index bf486d5a0bb8a..126ddb5454183 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -15,6 +15,7 @@ import ( "github.com/satori/go.uuid" "path/filepath" "github.com/markbates/goth/providers/github" + "github.com/markbates/goth/providers/gplus" ) var ( @@ -94,6 +95,8 @@ func createProvider(providerName, providerType, clientID, clientSecret string) g switch providerType { case "github": provider = github.New(clientID, clientSecret, callbackURL, "user:email") + case "gplus": + provider = gplus.New(clientID, clientSecret, callbackURL, "email") } // always set the name if provider is created so we can support multiple setups of 1 provider diff --git a/public/img/github.png b/public/img/auth/github.png similarity index 100% rename from public/img/github.png rename to public/img/auth/github.png diff --git a/public/img/auth/google_plus.png b/public/img/auth/google_plus.png new file mode 100644 index 0000000000000000000000000000000000000000..720824230a50490f008d5e0c1c1d167e6923d900 GIT binary patch literal 4366 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 0 { + for _, scope := range scopes { + c.Scopes = append(c.Scopes, scope) + } + } else { + c.Scopes = []string{"profile", "email", "openid"} + } + return c +} + +//RefreshTokenAvailable refresh token is provided by auth provider or not +func (p *Provider) RefreshTokenAvailable() bool { + return true +} + +//RefreshToken get new access token based on the refresh token +func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + token := &oauth2.Token{RefreshToken: refreshToken} + ts := p.config.TokenSource(goth.ContextForClient(p.Client()), token) + newToken, err := ts.Token() + if err != nil { + return nil, err + } + return newToken, err +} + +// SetPrompt sets the prompt values for the GPlus OAuth call. Use this to +// force users to choose and account every time by passing "select_account", +// for example. +// See https://developers.google.com/identity/protocols/OpenIDConnect#authenticationuriparameters +func (p *Provider) SetPrompt(prompt ...string) { + if len(prompt) == 0 { + return + } + p.prompt = oauth2.SetAuthURLParam("prompt", strings.Join(prompt, " ")) +} diff --git a/vendor/github.com/markbates/goth/providers/gplus/session.go b/vendor/github.com/markbates/goth/providers/gplus/session.go new file mode 100644 index 0000000000000..9710031f4d946 --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/gplus/session.go @@ -0,0 +1,61 @@ +package gplus + +import ( + "encoding/json" + "errors" + "strings" + "time" + + "github.com/markbates/goth" +) + +// Session stores data during the auth process with Google+. +type Session struct { + AuthURL string + AccessToken string + RefreshToken string + ExpiresAt time.Time +} + +// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Google+ provider. +func (s Session) GetAuthURL() (string, error) { + if s.AuthURL == "" { + return "", errors.New(goth.NoAuthUrlErrorMessage) + } + return s.AuthURL, nil +} + +// Authorize the session with Google+ and return the access token to be stored for future use. +func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { + p := provider.(*Provider) + token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code")) + if err != nil { + return "", err + } + + if !token.Valid() { + return "", errors.New("Invalid token received from provider") + } + + s.AccessToken = token.AccessToken + s.RefreshToken = token.RefreshToken + s.ExpiresAt = token.Expiry + return token.AccessToken, err +} + +// Marshal the session into a string +func (s Session) Marshal() string { + b, _ := json.Marshal(s) + return string(b) +} + +func (s Session) String() string { + return s.Marshal() +} + +// UnmarshalSession will unmarshal a JSON string into a session. +func (p *Provider) UnmarshalSession(data string) (goth.Session, error) { + sess := &Session{} + err := json.NewDecoder(strings.NewReader(data)).Decode(sess) + return sess, err +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 3a4ebc57809d8..1aa03f1d7dd36 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -568,6 +568,12 @@ "revision": "450379d2950a65070b23cc93c53436553add4484", "revisionTime": "2017-02-06T19:46:32Z" }, + { + "checksumSHA1": "cX6kR9y94BWFZvI/7UFrsFsP3FQ=", + "path": "github.com/markbates/goth/providers/gplus", + "revision": "a0a751ca505adde67bc2c92e2aae99531b1e3213", + "revisionTime": "2017-02-20T13:56:36Z" + }, { "checksumSHA1": "9FJUwn3EIgASVki+p8IHgWVC5vQ=", "path": "github.com/mattn/go-sqlite3", From a24e23899f14d86673621186f097797ad44f5fe7 Mon Sep 17 00:00:00 2001 From: willemvd Date: Tue, 21 Feb 2017 11:19:08 +0100 Subject: [PATCH 02/19] sort signin oauth2 providers based on the name so order is always the same --- models/login_source.go | 11 ++++++++--- routers/user/auth.go | 6 ++++-- templates/user/auth/signin_inner.tmpl | 7 +++++-- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/models/login_source.go b/models/login_source.go index 00b5d80c9f54c..4432f1ec18950 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/auth/pam" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/auth/oauth2" + "sort" ) // LoginType represents an login type. @@ -705,20 +706,24 @@ func GetActiveOAuth2LoginSourceByName(name string) (*LoginSource, error) { // GetActiveOAuth2Providers returns the map of configured active OAuth2 providers // key is used as technical name (like in the callbackURL) // values to display -func GetActiveOAuth2Providers() (map[string]OAuth2Provider, error) { +func GetActiveOAuth2Providers() ([]string, map[string]OAuth2Provider, error) { // Maybe also seperate used and unused providers so we can force the registration of only 1 active provider for each type loginSources, err := GetActiveOAuth2ProviderLoginSources() if err != nil { - return nil, err + return nil, nil, err } + var orderedKeys []string providers := make(map[string]OAuth2Provider) for _, source := range loginSources { providers[source.Name] = OAuth2Providers[source.OAuth2().Provider] + orderedKeys = append(orderedKeys, source.Name) } - return providers, nil + sort.Strings(orderedKeys) + + return orderedKeys, providers, nil } // InitOAuth2 initialize the OAuth2 lib and register all active OAuth2 providers in the library diff --git a/routers/user/auth.go b/routers/user/auth.go index 5b9297d3498ac..e8353de732fdb 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -114,11 +114,12 @@ func SignIn(ctx *context.Context) { return } - oauth2Providers, err := models.GetActiveOAuth2Providers() + orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers() if err != nil { ctx.Handle(500, "UserSignIn", err) return } + ctx.Data["OrderedOAuth2Names"] = orderedOAuth2Names ctx.Data["OAuth2Providers"] = oauth2Providers ctx.HTML(200, tplSignIn) @@ -128,11 +129,12 @@ func SignIn(ctx *context.Context) { func SignInPost(ctx *context.Context, form auth.SignInForm) { ctx.Data["Title"] = ctx.Tr("sign_in") - oauth2Providers, err := models.GetActiveOAuth2Providers() + orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers() if err != nil { ctx.Handle(500, "UserSignIn", err) return } + ctx.Data["OrderedOAuth2Names"] = orderedOAuth2Names ctx.Data["OAuth2Providers"] = oauth2Providers if ctx.HasError() { diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index 34fcea08fcbe1..300764d138a7f 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -41,11 +41,14 @@ {{end}} - {{if .OAuth2Providers}} + {{if and .OrderedOAuth2Names .OAuth2Providers}}
-

{{.i18n.Tr "sign_in_with"}}

{{range $key, $value := .OAuth2Providers}}{{$value.DisplayName}}{{end}} +

{{.i18n.Tr "sign_in_with"}}

{{range $key := .OrderedOAuth2Names}} + {{$provider := index $.OAuth2Providers $key}} + {{$provider.DisplayName}} + {{end}}
From 02638f02cf3bc83efed4f8337793d5f55722f4e1 Mon Sep 17 00:00:00 2001 From: willemvd Date: Tue, 21 Feb 2017 11:48:38 +0100 Subject: [PATCH 03/19] update auth tip for google+ --- options/locale/locale_en-US.ini | 5 ++++- templates/admin/auth/new.tmpl | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 9043ff12b6ca6..f23e71e2b8920 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1122,7 +1122,10 @@ auths.oauth2_clientID = Client ID (Key) auths.oauth2_clientSecret = Client Secret auths.enable_auto_register = Enable Auto Registration auths.tips = Tips -auths.tip.github = Register a new OAuth application on https://github.com/settings/applications/new and use /user/oauth2//callback as "Authorization callback URL" +auths.tips.oauth2.general = OAuth2 General +auths.tips.oauth2.general.tip = When registering a new OAuth2 application, the callback/redirect URL should be: /user/oauth2//callback +auths.tip.github = Register a new OAuth application on https://github.com/settings/applications/new +auths.tip.google_plus = Obtain OAuth2 client credentials from the Google API console (https://console.developers.google.com/) auths.edit = Edit Authentication Setting auths.activated = This authentication is activated auths.new_success = New authentication '%s' has been added successfully. diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl index 24257a1b65ad3..d16c443b4abc1 100644 --- a/templates/admin/auth/new.tmpl +++ b/templates/admin/auth/new.tmpl @@ -195,8 +195,12 @@
GMail Settings:

Host: smtp.gmail.com, Port: 587, Enable TLS Encryption: true

+
{{.i18n.Tr "admin.auths.tips.oauth2.general"}}:
+

{{.i18n.Tr "admin.auths.tips.oauth2.general.tip"}}

OAuth GitHub:

{{.i18n.Tr "admin.auths.tip.github"}}

+
OAuth2 Google+:
+

{{.i18n.Tr "admin.auths.tip.google_plus"}}

From 6c48e1600b1e524319b663017c5dd063cd697cb6 Mon Sep 17 00:00:00 2001 From: willemvd Date: Tue, 21 Feb 2017 11:51:42 +0100 Subject: [PATCH 04/19] add gitlab provider --- models/login_source.go | 3 +- modules/auth/oauth2/oauth2.go | 3 + options/locale/locale_en-US.ini | 1 + public/img/auth/gitlab.png | Bin 0 -> 3616 bytes templates/admin/auth/new.tmpl | 2 + .../markbates/goth/providers/gitlab/gitlab.go | 178 ++++++++++++++++++ .../goth/providers/gitlab/session.go | 63 +++++++ vendor/vendor.json | 6 + 8 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 public/img/auth/gitlab.png create mode 100644 vendor/github.com/markbates/goth/providers/gitlab/gitlab.go create mode 100644 vendor/github.com/markbates/goth/providers/gitlab/session.go diff --git a/models/login_source.go b/models/login_source.go index 4432f1ec18950..9b9db87eb3d2c 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -600,7 +600,8 @@ type OAuth2Provider struct { // value is used to store display data var OAuth2Providers = map[string]OAuth2Provider{ "github": {Name: "github", DisplayName:"GitHub", Image: "/img/auth/github.png"}, - "gplus": {Name: "gplus", DisplayName:"Google+", Image: "/img/auth/google_plus.png"}, + "gitlab": {Name: "gitlab", DisplayName:"GitLab", Image: "/img/auth/gitlab.png"}, + "gplus": {Name: "gplus", DisplayName:"Google+", Image: "/img/auth/google_plus.png"}, } // ExternalUserLogin attempts a login using external source types. diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index 126ddb5454183..f2950f36cd14a 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -16,6 +16,7 @@ import ( "path/filepath" "github.com/markbates/goth/providers/github" "github.com/markbates/goth/providers/gplus" + "github.com/markbates/goth/providers/gitlab" ) var ( @@ -95,6 +96,8 @@ func createProvider(providerName, providerType, clientID, clientSecret string) g switch providerType { case "github": provider = github.New(clientID, clientSecret, callbackURL, "user:email") + case "gitlab": + provider = gitlab.New(clientID, clientSecret, callbackURL) case "gplus": provider = gplus.New(clientID, clientSecret, callbackURL, "email") } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index f23e71e2b8920..58b2f97910ad6 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1125,6 +1125,7 @@ auths.tips = Tips auths.tips.oauth2.general = OAuth2 General auths.tips.oauth2.general.tip = When registering a new OAuth2 application, the callback/redirect URL should be: /user/oauth2//callback auths.tip.github = Register a new OAuth application on https://github.com/settings/applications/new +auths.tip.gitlab = Register a new application on https://gitlab.com/profile/applications auths.tip.google_plus = Obtain OAuth2 client credentials from the Google API console (https://console.developers.google.com/) auths.edit = Edit Authentication Setting auths.activated = This authentication is activated diff --git a/public/img/auth/gitlab.png b/public/img/auth/gitlab.png new file mode 100644 index 0000000000000000000000000000000000000000..ca30b45901932973038fa3cadf7f715292562962 GIT binary patch literal 3616 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 m0dRpdn^D8z@9GvSDpOPsRDKY0000{{.i18n.Tr "admin.auths.tips.oauth2.general.tip"}}

OAuth GitHub:

{{.i18n.Tr "admin.auths.tip.github"}}

+
OAuth2 GitLab:
+

{{.i18n.Tr "admin.auths.tip.gitlab"}}

OAuth2 Google+:

{{.i18n.Tr "admin.auths.tip.google_plus"}}

diff --git a/vendor/github.com/markbates/goth/providers/gitlab/gitlab.go b/vendor/github.com/markbates/goth/providers/gitlab/gitlab.go new file mode 100644 index 0000000000000..7e704ffed6484 --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/gitlab/gitlab.go @@ -0,0 +1,178 @@ +// Package gitlab implements the OAuth2 protocol for authenticating users through gitlab. +// This package can be used as a reference implementation of an OAuth2 provider for Goth. +package gitlab + +import ( + "bytes" + "encoding/json" + "io" + "io/ioutil" + "net/http" + "net/url" + "strconv" + + "github.com/markbates/goth" + "golang.org/x/oauth2" + "fmt" +) + +// These vars define the Authentication, Token, and Profile URLS for Gitlab. If +// using Gitlab CE or EE, you should change these values before calling New. +// +// Examples: +// gitlab.AuthURL = "https://gitlab.acme.com/oauth/authorize +// gitlab.TokenURL = "https://gitlab.acme.com/oauth/token +// gitlab.ProfileURL = "https://gitlab.acme.com/api/v3/user +var ( + AuthURL = "https://gitlab.com/oauth/authorize" + TokenURL = "https://gitlab.com/oauth/token" + ProfileURL = "https://gitlab.com/api/v3/user" +) + +// Provider is the implementation of `goth.Provider` for accessing Gitlab. +type Provider struct { + ClientKey string + Secret string + CallbackURL string + HTTPClient *http.Client + config *oauth2.Config + providerName string +} + +// New creates a new Gitlab provider and sets up important connection details. +// You should always call `gitlab.New` to get a new provider. Never try to +// create one manually. +func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { + p := &Provider{ + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + providerName: "gitlab", + } + p.config = newConfig(p, scopes) + return p +} + +// Name is the name used to retrieve this provider later. +func (p *Provider) Name() string { + return p.providerName +} + +// SetName is to update the name of the provider (needed in case of multiple providers of 1 type) +func (p *Provider) SetName(name string) { + p.providerName = name +} + +func (p *Provider) Client() *http.Client { + return goth.HTTPClientWithFallBack(p.HTTPClient) +} + +// Debug is a no-op for the gitlab package. +func (p *Provider) Debug(debug bool) {} + +// BeginAuth asks Gitlab for an authentication end-point. +func (p *Provider) BeginAuth(state string) (goth.Session, error) { + return &Session{ + AuthURL: p.config.AuthCodeURL(state), + }, nil +} + +// FetchUser will go to Gitlab and access basic information about the user. +func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { + sess := session.(*Session) + user := goth.User{ + AccessToken: sess.AccessToken, + Provider: p.Name(), + RefreshToken: sess.RefreshToken, + ExpiresAt: sess.ExpiresAt, + } + + if user.AccessToken == "" { + // data is not yet retrieved since accessToken is still empty + return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) + } + + response, err := p.Client().Get(ProfileURL + "?access_token=" + url.QueryEscape(sess.AccessToken)) + if err != nil { + if response != nil { + response.Body.Close() + } + return user, err + } + + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode) + } + + bits, err := ioutil.ReadAll(response.Body) + if err != nil { + return user, err + } + + err = json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData) + if err != nil { + return user, err + } + + err = userFromReader(bytes.NewReader(bits), &user) + + return user, err +} + +func newConfig(provider *Provider, scopes []string) *oauth2.Config { + c := &oauth2.Config{ + ClientID: provider.ClientKey, + ClientSecret: provider.Secret, + RedirectURL: provider.CallbackURL, + Endpoint: oauth2.Endpoint{ + AuthURL: AuthURL, + TokenURL: TokenURL, + }, + Scopes: []string{}, + } + + if len(scopes) > 0 { + for _, scope := range scopes { + c.Scopes = append(c.Scopes, scope) + } + } + return c +} + +func userFromReader(r io.Reader, user *goth.User) error { + u := struct { + Name string `json:"name"` + Email string `json:"email"` + NickName string `json:"username"` + ID int `json:"id"` + AvatarURL string `json:"avatar_url"` + }{} + err := json.NewDecoder(r).Decode(&u) + if err != nil { + return err + } + user.Email = u.Email + user.Name = u.Name + user.NickName = u.NickName + user.UserID = strconv.Itoa(u.ID) + user.AvatarURL = u.AvatarURL + return nil +} + +//RefreshTokenAvailable refresh token is provided by auth provider or not +func (p *Provider) RefreshTokenAvailable() bool { + return true +} + +//RefreshToken get new access token based on the refresh token +func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + token := &oauth2.Token{RefreshToken: refreshToken} + ts := p.config.TokenSource(goth.ContextForClient(p.Client()), token) + newToken, err := ts.Token() + if err != nil { + return nil, err + } + return newToken, err +} diff --git a/vendor/github.com/markbates/goth/providers/gitlab/session.go b/vendor/github.com/markbates/goth/providers/gitlab/session.go new file mode 100644 index 0000000000000..a2f90647c27ae --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/gitlab/session.go @@ -0,0 +1,63 @@ +package gitlab + +import ( + "encoding/json" + "errors" + "strings" + "time" + + "github.com/markbates/goth" +) + +// Session stores data during the auth process with Gitlab. +type Session struct { + AuthURL string + AccessToken string + RefreshToken string + ExpiresAt time.Time +} + +var _ goth.Session = &Session{} + +// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Gitlab provider. +func (s Session) GetAuthURL() (string, error) { + if s.AuthURL == "" { + return "", errors.New(goth.NoAuthUrlErrorMessage) + } + return s.AuthURL, nil +} + +// Authorize the session with Gitlab and return the access token to be stored for future use. +func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { + p := provider.(*Provider) + token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code")) + if err != nil { + return "", err + } + + if !token.Valid() { + return "", errors.New("Invalid token received from provider") + } + + s.AccessToken = token.AccessToken + s.RefreshToken = token.RefreshToken + s.ExpiresAt = token.Expiry + return token.AccessToken, err +} + +// Marshal the session into a string +func (s Session) Marshal() string { + b, _ := json.Marshal(s) + return string(b) +} + +func (s Session) String() string { + return s.Marshal() +} + +// UnmarshalSession wil unmarshal a JSON string into a session. +func (p *Provider) UnmarshalSession(data string) (goth.Session, error) { + s := &Session{} + err := json.NewDecoder(strings.NewReader(data)).Decode(s) + return s, err +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 1aa03f1d7dd36..c6001b7e49193 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -568,6 +568,12 @@ "revision": "450379d2950a65070b23cc93c53436553add4484", "revisionTime": "2017-02-06T19:46:32Z" }, + { + "checksumSHA1": "QO8bvOenTBbLPzr3ZvB7LCHJ0PY=", + "path": "github.com/markbates/goth/providers/gitlab", + "revision": "a0a751ca505adde67bc2c92e2aae99531b1e3213", + "revisionTime": "2017-02-20T13:56:36Z" + }, { "checksumSHA1": "cX6kR9y94BWFZvI/7UFrsFsP3FQ=", "path": "github.com/markbates/goth/providers/gplus", From 6a5b0c36443fab11163c2f50bffa2f719144330c Mon Sep 17 00:00:00 2001 From: willemvd Date: Tue, 21 Feb 2017 12:16:29 +0100 Subject: [PATCH 05/19] add bitbucket provider (and some go fmt) --- models/login_source.go | 13 +- modules/auth/oauth2/oauth2.go | 3 + options/locale/locale_en-US.ini | 1 + public/img/auth/bitbucket.png | Bin 0 -> 2161 bytes templates/admin/auth/new.tmpl | 2 + .../goth/providers/bitbucket/bitbucket.go | 206 ++++++++++++++++++ .../goth/providers/bitbucket/session.go | 61 ++++++ vendor/vendor.json | 6 + 8 files changed, 286 insertions(+), 6 deletions(-) create mode 100644 public/img/auth/bitbucket.png create mode 100644 vendor/github.com/markbates/goth/providers/bitbucket/bitbucket.go create mode 100644 vendor/github.com/markbates/goth/providers/bitbucket/session.go diff --git a/models/login_source.go b/models/login_source.go index 9b9db87eb3d2c..24f8409999777 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -499,7 +499,7 @@ func LoginViaSMTP(user *User, login, password string, sourceID int64, cfg *SMTPC idx := strings.Index(login, "@") if idx == -1 { return nil, ErrUserNotExist{0, login, 0} - } else if !com.IsSliceContainsStr(strings.Split(cfg.AllowedDomains, ","), login[idx + 1:]) { + } else if !com.IsSliceContainsStr(strings.Split(cfg.AllowedDomains, ","), login[idx+1:]) { return nil, ErrUserNotExist{0, login, 0} } } @@ -590,18 +590,19 @@ func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMCon // OAuth2Provider describes the display values of a single OAuth2 provider type OAuth2Provider struct { - Name string + Name string DisplayName string - Image string + Image string } // OAuth2Providers contains the map of registered OAuth2 providers in Gitea (based on goth) // key is used to map the OAuth2Provider with the goth provider type (also in LoginSource.OAuth2Config.Provider) // value is used to store display data var OAuth2Providers = map[string]OAuth2Provider{ - "github": {Name: "github", DisplayName:"GitHub", Image: "/img/auth/github.png"}, - "gitlab": {Name: "gitlab", DisplayName:"GitLab", Image: "/img/auth/gitlab.png"}, - "gplus": {Name: "gplus", DisplayName:"Google+", Image: "/img/auth/google_plus.png"}, + "bitbucket": {Name: "bitbucket", DisplayName: "Bitbucket", Image: "/img/auth/bitbucket.png"}, + "github": {Name: "github", DisplayName: "GitHub", Image: "/img/auth/github.png"}, + "gitlab": {Name: "gitlab", DisplayName: "GitLab", Image: "/img/auth/gitlab.png"}, + "gplus": {Name: "gplus", DisplayName: "Google+", Image: "/img/auth/google_plus.png"}, } // ExternalUserLogin attempts a login using external source types. diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index f2950f36cd14a..ef91e5f159894 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -17,6 +17,7 @@ import ( "github.com/markbates/goth/providers/github" "github.com/markbates/goth/providers/gplus" "github.com/markbates/goth/providers/gitlab" + "github.com/markbates/goth/providers/bitbucket" ) var ( @@ -94,6 +95,8 @@ func createProvider(providerName, providerType, clientID, clientSecret string) g var provider goth.Provider switch providerType { + case "bitbucket": + provider = bitbucket.New(clientID, clientSecret, callbackURL, "account") case "github": provider = github.New(clientID, clientSecret, callbackURL, "user:email") case "gitlab": diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 58b2f97910ad6..eb31185c36c29 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1124,6 +1124,7 @@ auths.enable_auto_register = Enable Auto Registration auths.tips = Tips auths.tips.oauth2.general = OAuth2 General auths.tips.oauth2.general.tip = When registering a new OAuth2 application, the callback/redirect URL should be: /user/oauth2//callback +auths.tip.bitbucket = Register a new OAuth consumer on https://bitbucket.org/account/user//oauth-consumers/new and add the permission "Account"-"Read" auths.tip.github = Register a new OAuth application on https://github.com/settings/applications/new auths.tip.gitlab = Register a new application on https://gitlab.com/profile/applications auths.tip.google_plus = Obtain OAuth2 client credentials from the Google API console (https://console.developers.google.com/) diff --git a/public/img/auth/bitbucket.png b/public/img/auth/bitbucket.png new file mode 100644 index 0000000000000000000000000000000000000000..b3d022a5a7019f199eaab18992c4bb5926824abe GIT binary patch literal 2161 zcmV-%2#)uOP)hr3xp!((jX#X5=Mhs^zX=z z4w#4IcmesHJjZd&MF-@^Z&1U~K05$CPcyaf-%uHoZ~@YX1;dBwVJS=x!-oaw<2=Tp zJbn!cA|!z94Ri=N>nYF==Y3jLEzpMiMr;@n?s*n{kQBlZm$$hNp%Utbbk%z4a5~@z zPV9#J%IKP~Uu(HcE41XHjQdvF!+AQcu&*vhj> z*Bx|@@8tBgbO^xZU6E31a7I220c`bLD6IX?VJcdnI8xweh_67yZ^(!W=!6Bhu5^YP zi~;G|#Neicn7jUf2Jun+0)^|)82uy!aFzhv!j+4|vuy@SgcPeIPKB5vP7(GI`wNw^ z6f)pB4M>VV#ULki0&r^;4rcvLFr97mf^O2WiF_8>V0T<@v@2?d-)0h!m>oiaSgg~I1r&p}s40mlO7 zDgr=C0b8j6A7oIH$*{NdxSnn3h|keR_a3kb1*EPXuowmCjY3ljJ-rVFEad6|F$GKx z0e^8I#J~nMB?m&kBl(G3H6WsZHUzv+0mTpyP*A{INWN)@{3^iC1@57MdfNTf|$SB}s3P@c7QVQ5sd%z$pg4zSNa0$>Juw)O5U{2hhhcAMQQb6Jo z5L3XU5b&36AW$*VgnuEqjsrp_4}M4T6LhTsrU-T*;B5)KGLqmuoJ!9ICLpu%v-?F@ zDCw^w`IcJ%!D}$$lGr^GeBB`40n36$7ghxC*OZ{*@94fHMv#0;0jHQMS~vM8!PqiQ zZhj zgghs0`X>J)7?%Q~ZUSr(?BJp|SI}gLy~mPr&0;ls;f>rGO1ke15hR~dz|j>1l;#(nQIa_r zlnwy}*j=uf$$Y=8AmFen0ul|? zS>J%MC?N6?5K%x1UXez90(hT$^|2Aq2B*q_ieJF-ykBe>0eoEJ{x5*oPXNJdFaZs& z$@|m+4PsB}(hV~nSPiTSCN-biM`nNDj;S09Y+Np8M9UA z1plxoj;HrBj1P_h|5iy}Vtfyb2qNMqfW&6|`M0_f$hUkpcL1w^mHFRSfbX>TcwisX zU%`BmgP#DEzy{xSZ%TS@rV2IM&E$a8JQ90#0rCuu3eI2`)$>E8j|iei1VrX|%01vJS>SB* zJ9?IQI#&6;z**onE{43pWIh95lh63s8{lbv_Y7XU3fme^;{t8-i;u@q>z!aki9N_$ zV0!b5d=9o|01eV2-HK0#0geX7B%|@OPS;}0`$&7+$?=(~B=-6}fWN@|@=QJtHss%= zOA>Q-(6iPgAeFYXmu(DQkec&0r3C2#D&byJgbF#SeJGek6D4}=%?-c*KVoJDhublU zydCb8nClBLmH#9e4HhDz;4^$A$MfR^I2<`HSc+{vqxT~=reh@LpaY?eb!bZpeO;07 z81$u_nQg*kcvKD$ezr6>F%IN4&w(Et>xU8pm%tcvF9gil?L?->vA4|P`mV=lu>iA3 z0SsCLMgw1)WPOIcZZVpu-|)CFZ>N!i*8mdH>XW7$_WKDDiRBTU{)P~sh45Sz(2uBq z)AjieB0w75b^r_5vM&TI;HUxpMM>uAg#aytBZ5z5fuldk%{heOdsYMF)v}2WMEU_8 z4{V^nesBRIVSjn2_oWD34cb>?>#2aq+-Obkp}xn1%5b;+K3GkX@3{aiilcMr`k^~$ z?}bj6%n$ghk1DbBsQ|t4NMH?ch1o|429v&&zxaq6L%#^nLrsQJz#?4HxfZ+xzV}!b n|DN;wYuLuZ6s+>2y7B)9_XxB^N(C3^00000NkvXXu0mjf9sA{< literal 0 HcmV?d00001 diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl index a8956effca07b..e08b4d4c0f00c 100644 --- a/templates/admin/auth/new.tmpl +++ b/templates/admin/auth/new.tmpl @@ -197,6 +197,8 @@

Host: smtp.gmail.com, Port: 587, Enable TLS Encryption: true

{{.i18n.Tr "admin.auths.tips.oauth2.general"}}:

{{.i18n.Tr "admin.auths.tips.oauth2.general.tip"}}

+
OAuth Bitbucket:
+

{{.i18n.Tr "admin.auths.tip.bitbucket"}}

OAuth GitHub:

{{.i18n.Tr "admin.auths.tip.github"}}

OAuth2 GitLab:
diff --git a/vendor/github.com/markbates/goth/providers/bitbucket/bitbucket.go b/vendor/github.com/markbates/goth/providers/bitbucket/bitbucket.go new file mode 100644 index 0000000000000..06d9c923cbc76 --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/bitbucket/bitbucket.go @@ -0,0 +1,206 @@ +// Package bitbucket implements the OAuth2 protocol for authenticating users through Bitbucket. +package bitbucket + +import ( + "bytes" + "encoding/json" + "io" + "io/ioutil" + "net/http" + "net/url" + + "github.com/markbates/goth" + "golang.org/x/oauth2" + "fmt" +) + +const ( + authURL string = "https://bitbucket.org/site/oauth2/authorize" + tokenURL string = "https://bitbucket.org/site/oauth2/access_token" + endpointProfile string = "https://api.bitbucket.org/2.0/user" + endpointEmail string = "https://api.bitbucket.org/2.0/user/emails" +) + +// New creates a new Bitbucket provider, and sets up important connection details. +// You should always call `bitbucket.New` to get a new Provider. Never try to create +// one manually. +func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { + p := &Provider{ + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + providerName: "bitbucket", + } + p.config = newConfig(p, scopes) + return p +} + +// Provider is the implementation of `goth.Provider` for accessing Bitbucket. +type Provider struct { + ClientKey string + Secret string + CallbackURL string + HTTPClient *http.Client + config *oauth2.Config + providerName string +} + +// Name is the name used to retrieve this provider later. +func (p *Provider) Name() string { + return p.providerName +} + +// SetName is to update the name of the provider (needed in case of multiple providers of 1 type) +func (p *Provider) SetName(name string) { + p.providerName = name +} + +func (p *Provider) Client() *http.Client { + return goth.HTTPClientWithFallBack(p.HTTPClient) +} + +// Debug is a no-op for the bitbucket package. +func (p *Provider) Debug(debug bool) {} + +// BeginAuth asks Bitbucket for an authentication end-point. +func (p *Provider) BeginAuth(state string) (goth.Session, error) { + url := p.config.AuthCodeURL(state) + session := &Session{ + AuthURL: url, + } + return session, nil +} + +// FetchUser will go to Bitbucket and access basic information about the user. +func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { + sess := session.(*Session) + user := goth.User{ + AccessToken: sess.AccessToken, + Provider: p.Name(), + RefreshToken: sess.RefreshToken, + ExpiresAt: sess.ExpiresAt, + } + + if user.AccessToken == "" { + // data is not yet retrieved since accessToken is still empty + return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) + } + + response, err := goth.HTTPClientWithFallBack(p.Client()).Get(endpointProfile + "?access_token=" + url.QueryEscape(sess.AccessToken)) + if err != nil { + return user, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode) + } + + bits, err := ioutil.ReadAll(response.Body) + if err != nil { + return user, err + } + + err = json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData) + if err != nil { + return user, err + } + + err = userFromReader(bytes.NewReader(bits), &user) + + response, err = goth.HTTPClientWithFallBack(p.Client()).Get(endpointEmail + "?access_token=" + url.QueryEscape(sess.AccessToken)) + if err != nil { + return user, err + } + defer response.Body.Close() + + bits, err = ioutil.ReadAll(response.Body) + if err != nil { + return user, err + } + + err = emailFromReader(bytes.NewReader(bits), &user) + return user, err +} + +func userFromReader(reader io.Reader, user *goth.User) error { + u := struct { + ID string `json:"uuid"` + Links struct { + Avatar struct { + URL string `json:"href"` + } `json:"avatar"` + } `json:"links"` + Email string `json:"email"` + Username string `json:"username"` + Name string `json:"display_name"` + Location string `json:"location"` + }{} + + err := json.NewDecoder(reader).Decode(&u) + if err != nil { + return err + } + + user.Name = u.Name + user.NickName = u.Username + user.AvatarURL = u.Links.Avatar.URL + user.UserID = u.ID + user.Location = u.Location + + return err +} + +func emailFromReader(reader io.Reader, user *goth.User) error { + e := struct { + Values []struct { + Email string `json:"email"` + } `json:"values"` + }{} + + err := json.NewDecoder(reader).Decode(&e) + if err != nil { + return err + } + + if len(e.Values) > 0 { + user.Email = e.Values[0].Email + } + + return err +} + +func newConfig(provider *Provider, scopes []string) *oauth2.Config { + c := &oauth2.Config{ + ClientID: provider.ClientKey, + ClientSecret: provider.Secret, + RedirectURL: provider.CallbackURL, + Endpoint: oauth2.Endpoint{ + AuthURL: authURL, + TokenURL: tokenURL, + }, + Scopes: []string{}, + } + + for _, scope := range scopes { + c.Scopes = append(c.Scopes, scope) + } + + return c +} + +//RefreshTokenAvailable refresh token is provided by auth provider or not +func (p *Provider) RefreshTokenAvailable() bool { + return true +} + +//RefreshToken get new access token based on the refresh token +func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + token := &oauth2.Token{RefreshToken: refreshToken} + ts := p.config.TokenSource(goth.ContextForClient(p.Client()), token) + newToken, err := ts.Token() + if err != nil { + return nil, err + } + return newToken, err +} diff --git a/vendor/github.com/markbates/goth/providers/bitbucket/session.go b/vendor/github.com/markbates/goth/providers/bitbucket/session.go new file mode 100644 index 0000000000000..a65242151bf11 --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/bitbucket/session.go @@ -0,0 +1,61 @@ +package bitbucket + +import ( + "encoding/json" + "errors" + "strings" + "time" + + "github.com/markbates/goth" +) + +// Session stores data during the auth process with Bitbucket. +type Session struct { + AuthURL string + AccessToken string + RefreshToken string + ExpiresAt time.Time +} + +// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Bitbucket provider. +func (s Session) GetAuthURL() (string, error) { + if s.AuthURL == "" { + return "", errors.New(goth.NoAuthUrlErrorMessage) + } + return s.AuthURL, nil +} + +// Authorize the session with Bitbucket and return the access token to be stored for future use. +func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { + p := provider.(*Provider) + token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code")) + if err != nil { + return "", err + } + + if !token.Valid() { + return "", errors.New("Invalid token received from provider") + } + + s.AccessToken = token.AccessToken + s.RefreshToken = token.RefreshToken + s.ExpiresAt = token.Expiry + return token.AccessToken, err +} + +// Marshal the session into a string +func (s Session) Marshal() string { + b, _ := json.Marshal(s) + return string(b) +} + +// UnmarshalSession will unmarshal a JSON string into a session. +func (p *Provider) UnmarshalSession(data string) (goth.Session, error) { + sess := &Session{} + err := json.NewDecoder(strings.NewReader(data)).Decode(sess) + return sess, err +} + +func (s Session) String() string { + return s.Marshal() +} diff --git a/vendor/vendor.json b/vendor/vendor.json index c6001b7e49193..bb363518ee760 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -562,6 +562,12 @@ "revision": "450379d2950a65070b23cc93c53436553add4484", "revisionTime": "2017-02-06T19:46:32Z" }, + { + "checksumSHA1": "crNSlQADjX6hcxykON2tFCqY4iw=", + "path": "github.com/markbates/goth/providers/bitbucket", + "revision": "7276be0fdf719ddff753f3574ef0f967e4a5a5f7", + "revisionTime": "2017-02-20T14:02:47Z" + }, { "checksumSHA1": "ZFqznX3/ZW65I4QeepiHQdE69nA=", "path": "github.com/markbates/goth/providers/github", From 213e67b98a655466792fdf5cd35296ffa6d2fb18 Mon Sep 17 00:00:00 2001 From: willemvd Date: Tue, 21 Feb 2017 13:38:09 +0100 Subject: [PATCH 06/19] add twitter provider --- models/login_source.go | 1 + modules/auth/oauth2/oauth2.go | 3 + options/locale/locale_en-US.ini | 1 + public/img/auth/twitter.png | Bin 0 -> 3110 bytes templates/admin/auth/new.tmpl | 2 + .../goth/providers/twitter/session.go | 54 + .../goth/providers/twitter/twitter.go | 160 ++ .../github.com/mrjones/oauth/MIT-LICENSE.txt | 7 + vendor/github.com/mrjones/oauth/README.md | 0 vendor/github.com/mrjones/oauth/oauth.go | 1406 +++++++++++++++++ vendor/github.com/mrjones/oauth/pre-commit.sh | 21 + vendor/github.com/mrjones/oauth/provider.go | 156 ++ vendor/vendor.json | 12 + 13 files changed, 1823 insertions(+) create mode 100644 public/img/auth/twitter.png create mode 100644 vendor/github.com/markbates/goth/providers/twitter/session.go create mode 100644 vendor/github.com/markbates/goth/providers/twitter/twitter.go create mode 100644 vendor/github.com/mrjones/oauth/MIT-LICENSE.txt create mode 100644 vendor/github.com/mrjones/oauth/README.md create mode 100644 vendor/github.com/mrjones/oauth/oauth.go create mode 100644 vendor/github.com/mrjones/oauth/pre-commit.sh create mode 100644 vendor/github.com/mrjones/oauth/provider.go diff --git a/models/login_source.go b/models/login_source.go index 24f8409999777..b1d2f703528ea 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -603,6 +603,7 @@ var OAuth2Providers = map[string]OAuth2Provider{ "github": {Name: "github", DisplayName: "GitHub", Image: "/img/auth/github.png"}, "gitlab": {Name: "gitlab", DisplayName: "GitLab", Image: "/img/auth/gitlab.png"}, "gplus": {Name: "gplus", DisplayName: "Google+", Image: "/img/auth/google_plus.png"}, + "twitter": {Name: "twitter", DisplayName: "Twitter", Image: "/img/auth/twitter.png"}, } // ExternalUserLogin attempts a login using external source types. diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index ef91e5f159894..5832a0da5434c 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -18,6 +18,7 @@ import ( "github.com/markbates/goth/providers/gplus" "github.com/markbates/goth/providers/gitlab" "github.com/markbates/goth/providers/bitbucket" + "github.com/markbates/goth/providers/twitter" ) var ( @@ -103,6 +104,8 @@ func createProvider(providerName, providerType, clientID, clientSecret string) g provider = gitlab.New(clientID, clientSecret, callbackURL) case "gplus": provider = gplus.New(clientID, clientSecret, callbackURL, "email") + case "twitter": + provider = twitter.NewAuthenticate(clientID, clientSecret, callbackURL) } // always set the name if provider is created so we can support multiple setups of 1 provider diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index eb31185c36c29..45c34d87d25e2 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1128,6 +1128,7 @@ auths.tip.bitbucket = Register a new OAuth consumer on https://bitbucket.org/acc auths.tip.github = Register a new OAuth application on https://github.com/settings/applications/new auths.tip.gitlab = Register a new application on https://gitlab.com/profile/applications auths.tip.google_plus = Obtain OAuth2 client credentials from the Google API console (https://console.developers.google.com/) +auths.tip.twitter = Go to https://dev.twitter.com/apps , create an application and ensure that the “Allow this application to be used to Sign in with Twitter” option is enabled. auths.edit = Edit Authentication Setting auths.activated = This authentication is activated auths.new_success = New authentication '%s' has been added successfully. diff --git a/public/img/auth/twitter.png b/public/img/auth/twitter.png new file mode 100644 index 0000000000000000000000000000000000000000..a4f14de57ae4a01d9ef9e905abb716504c8eb535 GIT binary patch literal 3110 zcmV+>4B7LEP)0lSX$UxSa@J#5F2al#D72#1hvw_RxA{3M4q!rAVJJ5yZh~LXJ&V2!4b`9 zS*u|Hvg>&>JTvKuE-ibUFEr6gBZG9tG|QL>g#y&HuLtVXeD71aYWw|bt%;(g#6wK|!Qa|i`jEeLLfbqvzS{O@A2^TUXVIWbdT-dJ!?;o!w5pM3M&^WWqfC&xD*{^JF=5Y8x`T)VY!d`ZX){f#)jsc`efi@$O5 z&4q)Wa`Yq#_;(4ob?}Jw@z7D5;=#k$#l?}O2p{*2uZ#ze+>jjaKV%iht}Rq*pBwhW zJBj4k-{D_*%i%x9x5&%om1@tD5`R=I)gCOI)0+v4W^P-k1kd~EQ4_Zk4$TSIY3McK zpw|ySt_R#ae9RUJx&qPIKV80>mXVCz2)H71Bb1R`wv}>kbA`e5nnHU zqLh82K791%ST3{0t48lg+@cb`)~iI{_WbZn$+hbPC+vznDN%0oc#(9l`^Goon+gZg z=(j(plNvXUdW3^cDZ%ay666;{M{SG?)mlS%eBDRLX~4OObp-oTEr<0&IeedRU}oAzOtJL*`|;&M!^iif z0?iyhq=)yLuzh1IlFmdCxRly0f8!h9xT$bPrhuX%u1?;R29@1>qE$8c0pWm1>YTfY zYs1T;a*rQ8To1Mi)29b#Cw$%-q64%A1wS&E`m1oz|JQ5oP^{M8U8shA)(cWV?bTe| zh_q21O`<`xi603En9e(V|7OC?63aN?xe`oUWy zfik!lh0r_XVl{Ga;eZ~TZM|T9Pc|4y)%;)!RsmaDYWnftgh`!xS!1 z-WaKmb~`A1J$13|>hbpp2NuVsq`^WZI(tYZrsd&Q5f%l}=)j?C$g3ABIDXjTgJD*| zjjY$f(rNMUwOgyrlNuGA-LL(^H&S7ARk0eqKsYerVQMi_4!_R>A>hFxto7R95gX$E z#Js|*_4^0jNbOm%1QreqZc6VXxNx$R+w1&OxnC~*wTnq&KvMkp(U(P1^5=^>-m;+d zzH;~sJT z+(zNjK?PL9uM3A|=2N$QvD#R{?7D}la0oF?D#DpmV5CnJXDO|3YKUim41zP>ztbj}2@>)>?FBf}L^Q_Oj6b;KBHg zm31omb3g87sju01Ci~J;ns~7u=Dov*uez;po*%qajV$LO(dJxAQiM#~Xiyf3*>^^G zE8rRmj^OPg8}=KJ^}biFeS(P2h)`zwUFFJ`=^?+wG!(hts>8(qLgR|WBgwEsGCf~7 zY82rN$oxV<;`R6}W-tG@q2y{^xZV78F)4tcoEAV!3A&=W1y~`(EQy3&R~%iM1V3tK z{FG1g)5QWVfCb2+`F+Ma{N=Y1S)ex{3kbg8ijs_hraSaYDXctO~&V8S^*R z5w3lvaKjhGw-7FmeaV7h!4HPH*k)4}sS@^6d@0?YV`C4KFOPkhQYULv_)jJg@uS72 z1glw6SN^_eDZDAbm^wpT364Ptg@*8ZMZC}zVFeKBT%UU^|C@_9qvdqf`;MBpqb4j% zXcA~RRYZM(Tfl*V#t`^qTW7q!37Vz{O$l7cKtc_#9X@7jEQ_!LX`#{>01^Gm!LZ13 ze=A!4!J?+n2yy9Mw0QARHGV83Ijiv(AO|Sk$YcZl^1F)6|ECoLz|01`a`;x6lGp1` zJg1lpmZHzfEiNorEMOvYVHH1HWQ!{Q^vVHhFWR1@P&vXt(@>ZJMAV-Q)-|fVqH_u)xOsE0Vn5Qwp9dnneCtWC1WO z4*oB?m*T;BD8WnD;cw`|$Y!N6e@1;a^$ljd)8U{em^R;Cx`u($@tQ2LeL4G3#0?x2 z_YJWtL>6k#In#X%(>#$zv52#7!*m%duJ{}kV!8r*0Oncvz3IB*gl%}5%|h+8Pqc9Y zc%|^^a)sY0J%-E|YHyBhQi93(2zqJC+iLaxY&UexdN&soOIJU*rxLzRRpLj)uUTB} z_ynr$1H$4;!CQpOtv}9;nO7Hndir|c79mo!KrA%o_pu#Gj02z0YbhHZl3gR^i8pzE_%&vTBdDh6bTl7$m?jcV)=k;)u$i95{K)&5 zyX_b9+S=nn?b><-3Z~d%M|hzWzDzin=41d5jSy0jr9KJT$(xPYi2!)q$^te@e?S{9E;z;e$+IIduJygIh5qzAbGMfuP z0k)x$f@d}p7S@&pH+q^vim5iWc@h5~VEc_AzvaE{(75@R;rUA*LJu&!l)$?%N!%c} zR%q-0My43t17$V&dyGqAUAPp(Y#m?69$vZLJDGYD4lE-H*GaN3ERW@V4DC0{-r&>h zv1g^#CLZ{Q1x%LQ%x|JK;{w|RfwB-3<>_2LErzg@`wIRc!t;Y-@xS3Cgfr+6dye@a zf57U3re?>|8F^R_|3UWnN{~@jU-kPkeXfv_cBb2E){ATO_w#k$aT59;`uz{{.i18n.Tr "admin.auths.tip.gitlab"}}

OAuth2 Google+:

{{.i18n.Tr "admin.auths.tip.google_plus"}}

+
OAuth2 Twitter:
+

{{.i18n.Tr "admin.auths.tip.twitter"}}

diff --git a/vendor/github.com/markbates/goth/providers/twitter/session.go b/vendor/github.com/markbates/goth/providers/twitter/session.go new file mode 100644 index 0000000000000..049928ff26253 --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/twitter/session.go @@ -0,0 +1,54 @@ +package twitter + +import ( + "encoding/json" + "errors" + "strings" + + "github.com/markbates/goth" + "github.com/mrjones/oauth" +) + +// Session stores data during the auth process with Twitter. +type Session struct { + AuthURL string + AccessToken *oauth.AccessToken + RequestToken *oauth.RequestToken +} + +// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Twitter provider. +func (s Session) GetAuthURL() (string, error) { + if s.AuthURL == "" { + return "", errors.New(goth.NoAuthUrlErrorMessage) + } + return s.AuthURL, nil +} + +// Authorize the session with Twitter and return the access token to be stored for future use. +func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { + p := provider.(*Provider) + accessToken, err := p.consumer.AuthorizeToken(s.RequestToken, params.Get("oauth_verifier")) + if err != nil { + return "", err + } + + s.AccessToken = accessToken + return accessToken.Token, err +} + +// Marshal the session into a string +func (s Session) Marshal() string { + b, _ := json.Marshal(s) + return string(b) +} + +func (s Session) String() string { + return s.Marshal() +} + +// UnmarshalSession will unmarshal a JSON string into a session. +func (p *Provider) UnmarshalSession(data string) (goth.Session, error) { + sess := &Session{} + err := json.NewDecoder(strings.NewReader(data)).Decode(sess) + return sess, err +} diff --git a/vendor/github.com/markbates/goth/providers/twitter/twitter.go b/vendor/github.com/markbates/goth/providers/twitter/twitter.go new file mode 100644 index 0000000000000..3703f21974b60 --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/twitter/twitter.go @@ -0,0 +1,160 @@ +// Package twitter implements the OAuth protocol for authenticating users through Twitter. +// This package can be used as a reference implementation of an OAuth provider for Goth. +package twitter + +import ( + "bytes" + "encoding/json" + "errors" + "io/ioutil" + "net/http" + + "github.com/markbates/goth" + "github.com/mrjones/oauth" + "golang.org/x/oauth2" + "fmt" +) + +var ( + requestURL = "https://api.twitter.com/oauth/request_token" + authorizeURL = "https://api.twitter.com/oauth/authorize" + authenticateURL = "https://api.twitter.com/oauth/authenticate" + tokenURL = "https://api.twitter.com/oauth/access_token" + endpointProfile = "https://api.twitter.com/1.1/account/verify_credentials.json" +) + +// New creates a new Twitter provider, and sets up important connection details. +// You should always call `twitter.New` to get a new Provider. Never try to create +// one manually. +// +// If you'd like to use authenticate instead of authorize, use NewAuthenticate instead. +func New(clientKey, secret, callbackURL string) *Provider { + p := &Provider{ + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + providerName: "twitter", + } + p.consumer = newConsumer(p, authorizeURL) + return p +} + +// NewAuthenticate is the almost same as New. +// NewAuthenticate uses the authenticate URL instead of the authorize URL. +func NewAuthenticate(clientKey, secret, callbackURL string) *Provider { + p := &Provider{ + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + providerName: "twitter", + } + p.consumer = newConsumer(p, authenticateURL) + return p +} + +// Provider is the implementation of `goth.Provider` for accessing Twitter. +type Provider struct { + ClientKey string + Secret string + CallbackURL string + HTTPClient *http.Client + debug bool + consumer *oauth.Consumer + providerName string +} + +// Name is the name used to retrieve this provider later. +func (p *Provider) Name() string { + return p.providerName +} + +// SetName is to update the name of the provider (needed in case of multiple providers of 1 type) +func (p *Provider) SetName(name string) { + p.providerName = name +} + +func (p *Provider) Client() *http.Client { + return goth.HTTPClientWithFallBack(p.HTTPClient) +} + +// Debug sets the logging of the OAuth client to verbose. +func (p *Provider) Debug(debug bool) { + p.debug = debug +} + +// BeginAuth asks Twitter for an authentication end-point and a request token for a session. +// Twitter does not support the "state" variable. +func (p *Provider) BeginAuth(state string) (goth.Session, error) { + requestToken, url, err := p.consumer.GetRequestTokenAndUrl(p.CallbackURL) + session := &Session{ + AuthURL: url, + RequestToken: requestToken, + } + return session, err +} + +// FetchUser will go to Twitter and access basic information about the user. +func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { + sess := session.(*Session) + user := goth.User{ + Provider: p.Name(), + } + + if sess.AccessToken == nil { + // data is not yet retrieved since accessToken is still empty + return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) + } + + response, err := p.consumer.Get( + endpointProfile, + map[string]string{"include_entities": "false", "skip_status": "true"}, + sess.AccessToken) + if err != nil { + return user, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode) + } + + bits, err := ioutil.ReadAll(response.Body) + err = json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData) + if err != nil { + return user, err + } + + user.Name = user.RawData["name"].(string) + user.NickName = user.RawData["screen_name"].(string) + user.Description = user.RawData["description"].(string) + user.AvatarURL = user.RawData["profile_image_url"].(string) + user.UserID = user.RawData["id_str"].(string) + user.Location = user.RawData["location"].(string) + user.AccessToken = sess.AccessToken.Token + user.AccessTokenSecret = sess.AccessToken.Secret + return user, err +} + +func newConsumer(provider *Provider, authURL string) *oauth.Consumer { + c := oauth.NewConsumer( + provider.ClientKey, + provider.Secret, + oauth.ServiceProvider{ + RequestTokenUrl: requestURL, + AuthorizeTokenUrl: authURL, + AccessTokenUrl: tokenURL, + }) + + c.Debug(provider.debug) + return c +} + +//RefreshToken refresh token is not provided by twitter +func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + return nil, errors.New("Refresh token is not provided by twitter") +} + +//RefreshTokenAvailable refresh token is not provided by twitter +func (p *Provider) RefreshTokenAvailable() bool { + return false +} diff --git a/vendor/github.com/mrjones/oauth/MIT-LICENSE.txt b/vendor/github.com/mrjones/oauth/MIT-LICENSE.txt new file mode 100644 index 0000000000000..6c9461e6c6781 --- /dev/null +++ b/vendor/github.com/mrjones/oauth/MIT-LICENSE.txt @@ -0,0 +1,7 @@ +Copyright (C) 2013 Matthew R. Jones + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/mrjones/oauth/README.md b/vendor/github.com/mrjones/oauth/README.md new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/vendor/github.com/mrjones/oauth/oauth.go b/vendor/github.com/mrjones/oauth/oauth.go new file mode 100644 index 0000000000000..6ae39f2376a57 --- /dev/null +++ b/vendor/github.com/mrjones/oauth/oauth.go @@ -0,0 +1,1406 @@ +// OAuth 1.0 consumer implementation. +// See http://www.oauth.net and RFC 5849 +// +// There are typically three parties involved in an OAuth exchange: +// (1) The "Service Provider" (e.g. Google, Twitter, NetFlix) who operates the +// service where the data resides. +// (2) The "End User" who owns that data, and wants to grant access to a third-party. +// (3) That third-party who wants access to the data (after first being authorized by +// the user). This third-party is referred to as the "Consumer" in OAuth +// terminology. +// +// This library is designed to help implement the third-party consumer by handling the +// low-level authentication tasks, and allowing for authenticated requests to the +// service provider on behalf of the user. +// +// Caveats: +// - Currently only supports HMAC and RSA signatures. +// - Currently only supports SHA1 and SHA256 hashes. +// - Currently only supports OAuth 1.0 +// +// Overview of how to use this library: +// (1) First create a new Consumer instance with the NewConsumer function +// (2) Get a RequestToken, and "authorization url" from GetRequestTokenAndUrl() +// (3) Save the RequestToken, you will need it again in step 6. +// (4) Redirect the user to the "authorization url" from step 2, where they will +// authorize your access to the service provider. +// (5) Wait. You will be called back on the CallbackUrl that you provide, and you +// will recieve a "verification code". +// (6) Call AuthorizeToken() with the RequestToken from step 2 and the +// "verification code" from step 5. +// (7) You will get back an AccessToken. Save this for as long as you need access +// to the user's data, and treat it like a password; it is a secret. +// (8) You can now throw away the RequestToken from step 2, it is no longer +// necessary. +// (9) Call "MakeHttpClient" using the AccessToken from step 7 to get an +// HTTP client which can access protected resources. +package oauth + +import ( + "bytes" + "crypto" + "crypto/hmac" + cryptoRand "crypto/rand" + "crypto/rsa" + "encoding/base64" + "errors" + "fmt" + "io" + "io/ioutil" + "math/rand" + "mime/multipart" + "net/http" + "net/url" + "sort" + "strconv" + "strings" + "sync" + "time" +) + +const ( + OAUTH_VERSION = "1.0" + SIGNATURE_METHOD_HMAC = "HMAC-" + SIGNATURE_METHOD_RSA = "RSA-" + + HTTP_AUTH_HEADER = "Authorization" + OAUTH_HEADER = "OAuth " + BODY_HASH_PARAM = "oauth_body_hash" + CALLBACK_PARAM = "oauth_callback" + CONSUMER_KEY_PARAM = "oauth_consumer_key" + NONCE_PARAM = "oauth_nonce" + SESSION_HANDLE_PARAM = "oauth_session_handle" + SIGNATURE_METHOD_PARAM = "oauth_signature_method" + SIGNATURE_PARAM = "oauth_signature" + TIMESTAMP_PARAM = "oauth_timestamp" + TOKEN_PARAM = "oauth_token" + TOKEN_SECRET_PARAM = "oauth_token_secret" + VERIFIER_PARAM = "oauth_verifier" + VERSION_PARAM = "oauth_version" +) + +var HASH_METHOD_MAP = map[crypto.Hash]string{ + crypto.SHA1: "SHA1", + crypto.SHA256: "SHA256", +} + +// TODO(mrjones) Do we definitely want separate "Request" and "Access" token classes? +// They're identical structurally, but used for different purposes. +type RequestToken struct { + Token string + Secret string +} + +type AccessToken struct { + Token string + Secret string + AdditionalData map[string]string +} + +type DataLocation int + +const ( + LOC_BODY DataLocation = iota + 1 + LOC_URL + LOC_MULTIPART + LOC_JSON + LOC_XML +) + +// Information about how to contact the service provider (see #1 above). +// You usually find all of these URLs by reading the documentation for the service +// that you're trying to connect to. +// Some common examples are: +// (1) Google, standard APIs: +// http://code.google.com/apis/accounts/docs/OAuth_ref.html +// - RequestTokenUrl: https://www.google.com/accounts/OAuthGetRequestToken +// - AuthorizeTokenUrl: https://www.google.com/accounts/OAuthAuthorizeToken +// - AccessTokenUrl: https://www.google.com/accounts/OAuthGetAccessToken +// Note: Some Google APIs (for example, Google Latitude) use different values for +// one or more of those URLs. +// (2) Twitter API: +// http://dev.twitter.com/pages/auth +// - RequestTokenUrl: http://api.twitter.com/oauth/request_token +// - AuthorizeTokenUrl: https://api.twitter.com/oauth/authorize +// - AccessTokenUrl: https://api.twitter.com/oauth/access_token +// (3) NetFlix API: +// http://developer.netflix.com/docs/Security +// - RequestTokenUrl: http://api.netflix.com/oauth/request_token +// - AuthroizeTokenUrl: https://api-user.netflix.com/oauth/login +// - AccessTokenUrl: http://api.netflix.com/oauth/access_token +// Set HttpMethod if the service provider requires a different HTTP method +// to be used for OAuth token requests +type ServiceProvider struct { + RequestTokenUrl string + AuthorizeTokenUrl string + AccessTokenUrl string + HttpMethod string + BodyHash bool + IgnoreTimestamp bool +} + +func (sp *ServiceProvider) httpMethod() string { + if sp.HttpMethod != "" { + return sp.HttpMethod + } + + return "GET" +} + +// lockedNonceGenerator wraps a non-reentrant random number generator with a +// lock +type lockedNonceGenerator struct { + nonceGenerator nonceGenerator + lock sync.Mutex +} + +func newLockedNonceGenerator(c clock) *lockedNonceGenerator { + return &lockedNonceGenerator{ + nonceGenerator: rand.New(rand.NewSource(c.Nanos())), + } +} + +func (n *lockedNonceGenerator) Int63() int64 { + n.lock.Lock() + r := n.nonceGenerator.Int63() + n.lock.Unlock() + return r +} + +// Consumers are stateless, you can call the various methods (GetRequestTokenAndUrl, +// AuthorizeToken, and Get) on various different instances of Consumers *as long as +// they were set up in the same way.* It is up to you, as the caller to persist the +// necessary state (RequestTokens and AccessTokens). +type Consumer struct { + // Some ServiceProviders require extra parameters to be passed for various reasons. + // For example Google APIs require you to set a scope= parameter to specify how much + // access is being granted. The proper values for scope= depend on the service: + // For more, see: http://code.google.com/apis/accounts/docs/OAuth.html#prepScope + AdditionalParams map[string]string + + // The rest of this class is configured via the NewConsumer function. + consumerKey string + serviceProvider ServiceProvider + + // Some APIs (e.g. Netflix) aren't quite standard OAuth, and require passing + // additional parameters when authorizing the request token. For most APIs + // this field can be ignored. For Netflix, do something like: + // consumer.AdditionalAuthorizationUrlParams = map[string]string{ + // "application_name": "YourAppName", + // "oauth_consumer_key": "YourConsumerKey", + // } + AdditionalAuthorizationUrlParams map[string]string + + debug bool + + // Defaults to http.Client{}, can be overridden (e.g. for testing) as necessary + HttpClient HttpClient + + // Some APIs (e.g. Intuit/Quickbooks) require sending additional headers along with + // requests. (like "Accept" to specify the response type as XML or JSON) Note that this + // will only *add* headers, not set existing ones. + AdditionalHeaders map[string][]string + + // Private seams for mocking dependencies when testing + clock clock + // Seeded generators are not reentrant + nonceGenerator nonceGenerator + signer signer +} + +func newConsumer(consumerKey string, serviceProvider ServiceProvider, httpClient *http.Client) *Consumer { + clock := &defaultClock{} + if httpClient == nil { + httpClient = &http.Client{} + } + return &Consumer{ + consumerKey: consumerKey, + serviceProvider: serviceProvider, + clock: clock, + HttpClient: httpClient, + nonceGenerator: newLockedNonceGenerator(clock), + + AdditionalParams: make(map[string]string), + AdditionalAuthorizationUrlParams: make(map[string]string), + } +} + +// Creates a new Consumer instance, with a HMAC-SHA1 signer +// - consumerKey and consumerSecret: +// values you should obtain from the ServiceProvider when you register your +// application. +// +// - serviceProvider: +// see the documentation for ServiceProvider for how to create this. +// +func NewConsumer(consumerKey string, consumerSecret string, + serviceProvider ServiceProvider) *Consumer { + consumer := newConsumer(consumerKey, serviceProvider, nil) + + consumer.signer = &HMACSigner{ + consumerSecret: consumerSecret, + hashFunc: crypto.SHA1, + } + + return consumer +} + +// Creates a new Consumer instance, with a HMAC-SHA1 signer +// - consumerKey and consumerSecret: +// values you should obtain from the ServiceProvider when you register your +// application. +// +// - serviceProvider: +// see the documentation for ServiceProvider for how to create this. +// +// - httpClient: +// Provides a custom implementation of the httpClient used under the hood +// to make the request. This is especially useful if you want to use +// Google App Engine. +// +func NewCustomHttpClientConsumer(consumerKey string, consumerSecret string, + serviceProvider ServiceProvider, httpClient *http.Client) *Consumer { + consumer := newConsumer(consumerKey, serviceProvider, httpClient) + + consumer.signer = &HMACSigner{ + consumerSecret: consumerSecret, + hashFunc: crypto.SHA1, + } + + return consumer +} + +// Creates a new Consumer instance, with a HMAC signer +// - consumerKey and consumerSecret: +// values you should obtain from the ServiceProvider when you register your +// application. +// +// - hashFunc: +// the crypto.Hash to use for signatures +// +// - serviceProvider: +// see the documentation for ServiceProvider for how to create this. +// +// - httpClient: +// Provides a custom implementation of the httpClient used under the hood +// to make the request. This is especially useful if you want to use +// Google App Engine. Can be nil for default. +// +func NewCustomConsumer(consumerKey string, consumerSecret string, + hashFunc crypto.Hash, serviceProvider ServiceProvider, + httpClient *http.Client) *Consumer { + consumer := newConsumer(consumerKey, serviceProvider, httpClient) + + consumer.signer = &HMACSigner{ + consumerSecret: consumerSecret, + hashFunc: hashFunc, + } + + return consumer +} + +// Creates a new Consumer instance, with a RSA-SHA1 signer +// - consumerKey: +// value you should obtain from the ServiceProvider when you register your +// application. +// +// - privateKey: +// the private key to use for signatures +// +// - serviceProvider: +// see the documentation for ServiceProvider for how to create this. +// +func NewRSAConsumer(consumerKey string, privateKey *rsa.PrivateKey, + serviceProvider ServiceProvider) *Consumer { + consumer := newConsumer(consumerKey, serviceProvider, nil) + + consumer.signer = &RSASigner{ + privateKey: privateKey, + hashFunc: crypto.SHA1, + rand: cryptoRand.Reader, + } + + return consumer +} + +// Creates a new Consumer instance, with a RSA signer +// - consumerKey: +// value you should obtain from the ServiceProvider when you register your +// application. +// +// - privateKey: +// the private key to use for signatures +// +// - hashFunc: +// the crypto.Hash to use for signatures +// +// - serviceProvider: +// see the documentation for ServiceProvider for how to create this. +// +// - httpClient: +// Provides a custom implementation of the httpClient used under the hood +// to make the request. This is especially useful if you want to use +// Google App Engine. Can be nil for default. +// +func NewCustomRSAConsumer(consumerKey string, privateKey *rsa.PrivateKey, + hashFunc crypto.Hash, serviceProvider ServiceProvider, + httpClient *http.Client) *Consumer { + consumer := newConsumer(consumerKey, serviceProvider, httpClient) + + consumer.signer = &RSASigner{ + privateKey: privateKey, + hashFunc: hashFunc, + rand: cryptoRand.Reader, + } + + return consumer +} + +// Kicks off the OAuth authorization process. +// - callbackUrl: +// Authorizing a token *requires* redirecting to the service provider. This is the +// URL which the service provider will redirect the user back to after that +// authorization is completed. The service provider will pass back a verification +// code which is necessary to complete the rest of the process (in AuthorizeToken). +// Notes on callbackUrl: +// - Some (all?) service providers allow for setting "oob" (for out-of-band) as a +// callback url. If this is set the service provider will present the +// verification code directly to the user, and you must provide a place for +// them to copy-and-paste it into. +// - Otherwise, the user will be redirected to callbackUrl in the browser, and +// will append a "oauth_verifier=" parameter. +// +// This function returns: +// - rtoken: +// A temporary RequestToken, used during the authorization process. You must save +// this since it will be necessary later in the process when calling +// AuthorizeToken(). +// +// - url: +// A URL that you should redirect the user to in order that they may authorize you +// to the service provider. +// +// - err: +// Set only if there was an error, nil otherwise. +func (c *Consumer) GetRequestTokenAndUrl(callbackUrl string) (rtoken *RequestToken, loginUrl string, err error) { + return c.GetRequestTokenAndUrlWithParams(callbackUrl, c.AdditionalParams) +} + +func (c *Consumer) GetRequestTokenAndUrlWithParams(callbackUrl string, additionalParams map[string]string) (rtoken *RequestToken, loginUrl string, err error) { + params := c.baseParams(c.consumerKey, additionalParams) + if callbackUrl != "" { + params.Add(CALLBACK_PARAM, callbackUrl) + } + + req := &request{ + method: c.serviceProvider.httpMethod(), + url: c.serviceProvider.RequestTokenUrl, + oauthParams: params, + } + if _, err := c.signRequest(req, ""); err != nil { // We don't have a token secret for the key yet + return nil, "", err + } + + resp, err := c.getBody(c.serviceProvider.httpMethod(), c.serviceProvider.RequestTokenUrl, params) + if err != nil { + return nil, "", errors.New("getBody: " + err.Error()) + } + + requestToken, err := parseRequestToken(*resp) + if err != nil { + return nil, "", errors.New("parseRequestToken: " + err.Error()) + } + + loginParams := make(url.Values) + for k, v := range c.AdditionalAuthorizationUrlParams { + loginParams.Set(k, v) + } + loginParams.Set(TOKEN_PARAM, requestToken.Token) + + loginUrl = c.serviceProvider.AuthorizeTokenUrl + "?" + loginParams.Encode() + + return requestToken, loginUrl, nil +} + +// After the user has authorized you to the service provider, use this method to turn +// your temporary RequestToken into a permanent AccessToken. You must pass in two values: +// - rtoken: +// The RequestToken returned from GetRequestTokenAndUrl() +// +// - verificationCode: +// The string which passed back from the server, either as the oauth_verifier +// query param appended to callbackUrl *OR* a string manually entered by the user +// if callbackUrl is "oob" +// +// It will return: +// - atoken: +// A permanent AccessToken which can be used to access the user's data (until it is +// revoked by the user or the service provider). +// +// - err: +// Set only if there was an error, nil otherwise. +func (c *Consumer) AuthorizeToken(rtoken *RequestToken, verificationCode string) (atoken *AccessToken, err error) { + return c.AuthorizeTokenWithParams(rtoken, verificationCode, c.AdditionalParams) +} + +func (c *Consumer) AuthorizeTokenWithParams(rtoken *RequestToken, verificationCode string, additionalParams map[string]string) (atoken *AccessToken, err error) { + params := map[string]string{ + VERIFIER_PARAM: verificationCode, + TOKEN_PARAM: rtoken.Token, + } + return c.makeAccessTokenRequestWithParams(params, rtoken.Secret, additionalParams) +} + +// Use the service provider to refresh the AccessToken for a given session. +// Note that this is only supported for service providers that manage an +// authorization session (e.g. Yahoo). +// +// Most providers do not return the SESSION_HANDLE_PARAM needed to refresh +// the token. +// +// See http://oauth.googlecode.com/svn/spec/ext/session/1.0/drafts/1/spec.html +// for more information. +// - accessToken: +// The AccessToken returned from AuthorizeToken() +// +// It will return: +// - atoken: +// An AccessToken which can be used to access the user's data (until it is +// revoked by the user or the service provider). +// +// - err: +// Set if accessToken does not contain the SESSION_HANDLE_PARAM needed to +// refresh the token, or if an error occurred when making the request. +func (c *Consumer) RefreshToken(accessToken *AccessToken) (atoken *AccessToken, err error) { + params := make(map[string]string) + sessionHandle, ok := accessToken.AdditionalData[SESSION_HANDLE_PARAM] + if !ok { + return nil, errors.New("Missing " + SESSION_HANDLE_PARAM + " in access token.") + } + params[SESSION_HANDLE_PARAM] = sessionHandle + params[TOKEN_PARAM] = accessToken.Token + + return c.makeAccessTokenRequest(params, accessToken.Secret) +} + +// Use the service provider to obtain an AccessToken for a given session +// - params: +// The access token request paramters. +// +// - secret: +// Secret key to use when signing the access token request. +// +// It will return: +// - atoken +// An AccessToken which can be used to access the user's data (until it is +// revoked by the user or the service provider). +// +// - err: +// Set only if there was an error, nil otherwise. +func (c *Consumer) makeAccessTokenRequest(params map[string]string, secret string) (atoken *AccessToken, err error) { + return c.makeAccessTokenRequestWithParams(params, secret, c.AdditionalParams) +} + +func (c *Consumer) makeAccessTokenRequestWithParams(params map[string]string, secret string, additionalParams map[string]string) (atoken *AccessToken, err error) { + orderedParams := c.baseParams(c.consumerKey, additionalParams) + for key, value := range params { + orderedParams.Add(key, value) + } + + req := &request{ + method: c.serviceProvider.httpMethod(), + url: c.serviceProvider.AccessTokenUrl, + oauthParams: orderedParams, + } + if _, err := c.signRequest(req, secret); err != nil { + return nil, err + } + + resp, err := c.getBody(c.serviceProvider.httpMethod(), c.serviceProvider.AccessTokenUrl, orderedParams) + if err != nil { + return nil, err + } + + return parseAccessToken(*resp) +} + +type RoundTripper struct { + consumer *Consumer + token *AccessToken +} + +func (c *Consumer) MakeRoundTripper(token *AccessToken) (*RoundTripper, error) { + return &RoundTripper{consumer: c, token: token}, nil +} + +func (c *Consumer) MakeHttpClient(token *AccessToken) (*http.Client, error) { + return &http.Client{ + Transport: &RoundTripper{consumer: c, token: token}, + }, nil +} + +// ** DEPRECATED ** +// Please call Get on the http client returned by MakeHttpClient instead! +// +// Executes an HTTP Get, authorized via the AccessToken. +// - url: +// The base url, without any query params, which is being accessed +// +// - userParams: +// Any key=value params to be included in the query string +// +// - token: +// The AccessToken returned by AuthorizeToken() +// +// This method returns: +// - resp: +// The HTTP Response resulting from making this request. +// +// - err: +// Set only if there was an error, nil otherwise. +func (c *Consumer) Get(url string, userParams map[string]string, token *AccessToken) (resp *http.Response, err error) { + return c.makeAuthorizedRequest("GET", url, LOC_URL, "", userParams, token) +} + +func encodeUserParams(userParams map[string]string) string { + data := url.Values{} + for k, v := range userParams { + data.Add(k, v) + } + return data.Encode() +} + +// ** DEPRECATED ** +// Please call "Post" on the http client returned by MakeHttpClient instead +func (c *Consumer) PostForm(url string, userParams map[string]string, token *AccessToken) (resp *http.Response, err error) { + return c.PostWithBody(url, "", userParams, token) +} + +// ** DEPRECATED ** +// Please call "Post" on the http client returned by MakeHttpClient instead +func (c *Consumer) Post(url string, userParams map[string]string, token *AccessToken) (resp *http.Response, err error) { + return c.PostWithBody(url, "", userParams, token) +} + +// ** DEPRECATED ** +// Please call "Post" on the http client returned by MakeHttpClient instead +func (c *Consumer) PostWithBody(url string, body string, userParams map[string]string, token *AccessToken) (resp *http.Response, err error) { + return c.makeAuthorizedRequest("POST", url, LOC_BODY, body, userParams, token) +} + +// ** DEPRECATED ** +// Please call "Do" on the http client returned by MakeHttpClient instead +// (and set the "Content-Type" header explicitly in the http.Request) +func (c *Consumer) PostJson(url string, body string, token *AccessToken) (resp *http.Response, err error) { + return c.makeAuthorizedRequest("POST", url, LOC_JSON, body, nil, token) +} + +// ** DEPRECATED ** +// Please call "Do" on the http client returned by MakeHttpClient instead +// (and set the "Content-Type" header explicitly in the http.Request) +func (c *Consumer) PostXML(url string, body string, token *AccessToken) (resp *http.Response, err error) { + return c.makeAuthorizedRequest("POST", url, LOC_XML, body, nil, token) +} + +// ** DEPRECATED ** +// Please call "Do" on the http client returned by MakeHttpClient instead +// (and setup the multipart data explicitly in the http.Request) +func (c *Consumer) PostMultipart(url, multipartName string, multipartData io.ReadCloser, userParams map[string]string, token *AccessToken) (resp *http.Response, err error) { + return c.makeAuthorizedRequestReader("POST", url, LOC_MULTIPART, 0, multipartName, multipartData, userParams, token) +} + +// ** DEPRECATED ** +// Please call "Delete" on the http client returned by MakeHttpClient instead +func (c *Consumer) Delete(url string, userParams map[string]string, token *AccessToken) (resp *http.Response, err error) { + return c.makeAuthorizedRequest("DELETE", url, LOC_URL, "", userParams, token) +} + +// ** DEPRECATED ** +// Please call "Put" on the http client returned by MakeHttpClient instead +func (c *Consumer) Put(url string, body string, userParams map[string]string, token *AccessToken) (resp *http.Response, err error) { + return c.makeAuthorizedRequest("PUT", url, LOC_URL, body, userParams, token) +} + +func (c *Consumer) Debug(enabled bool) { + c.debug = enabled + c.signer.Debug(enabled) +} + +type pair struct { + key string + value string +} + +type pairs []pair + +func (p pairs) Len() int { return len(p) } +func (p pairs) Less(i, j int) bool { return p[i].key < p[j].key } +func (p pairs) Swap(i, j int) { p[i], p[j] = p[j], p[i] } + +// This function has basically turned into a backwards compatibility layer +// between the old API (where clients explicitly called consumer.Get() +// consumer.Post() etc), and the new API (which takes actual http.Requests) +// +// So, here we construct the appropriate HTTP request for the inputs. +func (c *Consumer) makeAuthorizedRequestReader(method string, urlString string, dataLocation DataLocation, contentLength int, multipartName string, body io.ReadCloser, userParams map[string]string, token *AccessToken) (resp *http.Response, err error) { + urlObject, err := url.Parse(urlString) + if err != nil { + return nil, err + } + + request := &http.Request{ + Method: method, + URL: urlObject, + Header: http.Header{}, + Body: body, + ContentLength: int64(contentLength), + } + + vals := url.Values{} + for k, v := range userParams { + vals.Add(k, v) + } + + if dataLocation != LOC_BODY { + request.URL.RawQuery = vals.Encode() + request.URL.RawQuery = strings.Replace( + request.URL.RawQuery, ";", "%3B", -1) + + } else { + // TODO(mrjones): validate that we're not overrideing an exising body? + request.Body = ioutil.NopCloser(strings.NewReader(vals.Encode())) + request.ContentLength = int64(len(vals.Encode())) + } + + for k, vs := range c.AdditionalHeaders { + for _, v := range vs { + request.Header.Set(k, v) + } + } + + if dataLocation == LOC_BODY { + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } + + if dataLocation == LOC_JSON { + request.Header.Set("Content-Type", "application/json") + } + + if dataLocation == LOC_XML { + request.Header.Set("Content-Type", "application/xml") + } + + if dataLocation == LOC_MULTIPART { + pipeReader, pipeWriter := io.Pipe() + writer := multipart.NewWriter(pipeWriter) + if request.URL.Host == "www.mrjon.es" && + request.URL.Path == "/unittest" { + writer.SetBoundary("UNITTESTBOUNDARY") + } + go func(body io.Reader) { + part, err := writer.CreateFormFile(multipartName, "/no/matter") + if err != nil { + writer.Close() + pipeWriter.CloseWithError(err) + return + } + _, err = io.Copy(part, body) + if err != nil { + writer.Close() + pipeWriter.CloseWithError(err) + return + } + writer.Close() + pipeWriter.Close() + }(body) + request.Body = pipeReader + request.Header.Set("Content-Type", writer.FormDataContentType()) + } + + rt := RoundTripper{consumer: c, token: token} + + resp, err = rt.RoundTrip(request) + if err != nil { + return resp, err + } + + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + defer resp.Body.Close() + bytes, _ := ioutil.ReadAll(resp.Body) + + return resp, HTTPExecuteError{ + RequestHeaders: "", + ResponseBodyBytes: bytes, + Status: resp.Status, + StatusCode: resp.StatusCode, + } + } + + return resp, nil +} + +// cloneReq clones the src http.Request, making deep copies of the Header and +// the URL but shallow copies of everything else +func cloneReq(src *http.Request) *http.Request { + dst := &http.Request{} + *dst = *src + + dst.Header = make(http.Header, len(src.Header)) + for k, s := range src.Header { + dst.Header[k] = append([]string(nil), s...) + } + + if src.URL != nil { + dst.URL = cloneURL(src.URL) + } + + return dst +} + +// cloneURL shallow clones the src *url.URL +func cloneURL(src *url.URL) *url.URL { + dst := &url.URL{} + *dst = *src + + return dst +} + +func canonicalizeUrl(u *url.URL) string { + var buf bytes.Buffer + buf.WriteString(u.Scheme) + buf.WriteString("://") + buf.WriteString(u.Host) + buf.WriteString(u.Path) + + return buf.String() +} + +func parseBody(request *http.Request) (map[string]string, error) { + userParams := map[string]string{} + + // TODO(mrjones): factor parameter extraction into a separate method + if request.Header.Get("Content-Type") != + "application/x-www-form-urlencoded" { + // Most of the time we get parameters from the query string: + for k, vs := range request.URL.Query() { + if len(vs) != 1 { + return nil, fmt.Errorf("Must have exactly one value per param") + } + + userParams[k] = vs[0] + } + } else { + // x-www-form-urlencoded parameters come from the body instead: + defer request.Body.Close() + originalBody, err := ioutil.ReadAll(request.Body) + if err != nil { + return nil, err + } + + // If there was a body, we have to re-install it + // (because we've ruined it by reading it). + request.Body = ioutil.NopCloser(bytes.NewReader(originalBody)) + + params, err := url.ParseQuery(string(originalBody)) + if err != nil { + return nil, err + } + + for k, vs := range params { + if len(vs) != 1 { + return nil, fmt.Errorf("Must have exactly one value per param") + } + + userParams[k] = vs[0] + } + } + + return userParams, nil +} + +func paramsToSortedPairs(params map[string]string) pairs { + // Sort parameters alphabetically + paramPairs := make(pairs, len(params)) + i := 0 + for key, value := range params { + paramPairs[i] = pair{key: key, value: value} + i++ + } + sort.Sort(paramPairs) + + return paramPairs +} + +func calculateBodyHash(request *http.Request, s signer) (string, error) { + if request.Header.Get("Content-Type") == + "application/x-www-form-urlencoded" { + return "", nil + } + + var originalBody []byte + + if request.Body != nil { + var err error + + defer request.Body.Close() + originalBody, err = ioutil.ReadAll(request.Body) + if err != nil { + return "", err + } + + // If there was a body, we have to re-install it + // (because we've ruined it by reading it). + request.Body = ioutil.NopCloser(bytes.NewReader(originalBody)) + } + + h := s.HashFunc().New() + h.Write(originalBody) + rawSignature := h.Sum(nil) + + return base64.StdEncoding.EncodeToString(rawSignature), nil +} + +func (rt *RoundTripper) RoundTrip(userRequest *http.Request) (*http.Response, error) { + serverRequest := cloneReq(userRequest) + + allParams := rt.consumer.baseParams( + rt.consumer.consumerKey, rt.consumer.AdditionalParams) + + // Do not add the "oauth_token" parameter, if the access token has not been + // specified. By omitting this parameter when it is not specified, allows + // two-legged OAuth calls. + if len(rt.token.Token) > 0 { + allParams.Add(TOKEN_PARAM, rt.token.Token) + } + + if rt.consumer.serviceProvider.BodyHash { + bodyHash, err := calculateBodyHash(serverRequest, rt.consumer.signer) + if err != nil { + return nil, err + } + + if bodyHash != "" { + allParams.Add(BODY_HASH_PARAM, bodyHash) + } + } + + authParams := allParams.Clone() + + // TODO(mrjones): put these directly into the paramPairs below? + userParams, err := parseBody(serverRequest) + if err != nil { + return nil, err + } + paramPairs := paramsToSortedPairs(userParams) + + for i := range paramPairs { + allParams.Add(paramPairs[i].key, paramPairs[i].value) + } + + signingURL := cloneURL(serverRequest.URL) + if host := serverRequest.Host; host != "" { + signingURL.Host = host + } + baseString := rt.consumer.requestString(serverRequest.Method, canonicalizeUrl(signingURL), allParams) + + signature, err := rt.consumer.signer.Sign(baseString, rt.token.Secret) + if err != nil { + return nil, err + } + + authParams.Add(SIGNATURE_PARAM, signature) + + // Set auth header. + oauthHdr := OAUTH_HEADER + for pos, key := range authParams.Keys() { + for innerPos, value := range authParams.Get(key) { + if pos+innerPos > 0 { + oauthHdr += "," + } + oauthHdr += key + "=\"" + value + "\"" + } + } + serverRequest.Header.Add(HTTP_AUTH_HEADER, oauthHdr) + + if rt.consumer.debug { + fmt.Printf("Request: %v\n", serverRequest) + } + + resp, err := rt.consumer.HttpClient.Do(serverRequest) + + if err != nil { + return resp, err + } + + return resp, nil +} + +func (c *Consumer) makeAuthorizedRequest(method string, url string, dataLocation DataLocation, body string, userParams map[string]string, token *AccessToken) (resp *http.Response, err error) { + return c.makeAuthorizedRequestReader(method, url, dataLocation, len(body), "", ioutil.NopCloser(strings.NewReader(body)), userParams, token) +} + +type request struct { + method string + url string + oauthParams *OrderedParams + userParams map[string]string +} + +type HttpClient interface { + Do(req *http.Request) (resp *http.Response, err error) +} + +type clock interface { + Seconds() int64 + Nanos() int64 +} + +type nonceGenerator interface { + Int63() int64 +} + +type key interface { + String() string +} + +type signer interface { + Sign(message string, tokenSecret string) (string, error) + Verify(message string, signature string) error + SignatureMethod() string + HashFunc() crypto.Hash + Debug(enabled bool) +} + +type defaultClock struct{} + +func (*defaultClock) Seconds() int64 { + return time.Now().Unix() +} + +func (*defaultClock) Nanos() int64 { + return time.Now().UnixNano() +} + +func (c *Consumer) signRequest(req *request, tokenSecret string) (*request, error) { + baseString := c.requestString(req.method, req.url, req.oauthParams) + + signature, err := c.signer.Sign(baseString, tokenSecret) + if err != nil { + return nil, err + } + + req.oauthParams.Add(SIGNATURE_PARAM, signature) + return req, nil +} + +// Obtains an AccessToken from the response of a service provider. +// - data: +// The response body. +// +// This method returns: +// - atoken: +// The AccessToken generated from the response body. +// +// - err: +// Set if an AccessToken could not be parsed from the given input. +func parseAccessToken(data string) (atoken *AccessToken, err error) { + parts, err := url.ParseQuery(data) + if err != nil { + return nil, err + } + + tokenParam := parts[TOKEN_PARAM] + parts.Del(TOKEN_PARAM) + if len(tokenParam) < 1 { + return nil, errors.New("Missing " + TOKEN_PARAM + " in response. " + + "Full response body: '" + data + "'") + } + tokenSecretParam := parts[TOKEN_SECRET_PARAM] + parts.Del(TOKEN_SECRET_PARAM) + if len(tokenSecretParam) < 1 { + return nil, errors.New("Missing " + TOKEN_SECRET_PARAM + " in response." + + "Full response body: '" + data + "'") + } + + additionalData := parseAdditionalData(parts) + + return &AccessToken{tokenParam[0], tokenSecretParam[0], additionalData}, nil +} + +func parseRequestToken(data string) (*RequestToken, error) { + parts, err := url.ParseQuery(data) + if err != nil { + return nil, err + } + + tokenParam := parts[TOKEN_PARAM] + if len(tokenParam) < 1 { + return nil, errors.New("Missing " + TOKEN_PARAM + " in response. " + + "Full response body: '" + data + "'") + } + tokenSecretParam := parts[TOKEN_SECRET_PARAM] + if len(tokenSecretParam) < 1 { + return nil, errors.New("Missing " + TOKEN_SECRET_PARAM + " in response." + + "Full response body: '" + data + "'") + } + return &RequestToken{tokenParam[0], tokenSecretParam[0]}, nil +} + +func (c *Consumer) baseParams(consumerKey string, additionalParams map[string]string) *OrderedParams { + params := NewOrderedParams() + params.Add(VERSION_PARAM, OAUTH_VERSION) + params.Add(SIGNATURE_METHOD_PARAM, c.signer.SignatureMethod()) + params.Add(TIMESTAMP_PARAM, strconv.FormatInt(c.clock.Seconds(), 10)) + params.Add(NONCE_PARAM, strconv.FormatInt(c.nonceGenerator.Int63(), 10)) + params.Add(CONSUMER_KEY_PARAM, consumerKey) + for key, value := range additionalParams { + params.Add(key, value) + } + return params +} + +func parseAdditionalData(parts url.Values) map[string]string { + params := make(map[string]string) + for key, value := range parts { + if len(value) > 0 { + params[key] = value[0] + } + } + return params +} + +type HMACSigner struct { + consumerSecret string + hashFunc crypto.Hash + debug bool +} + +func (s *HMACSigner) Debug(enabled bool) { + s.debug = enabled +} + +func (s *HMACSigner) Sign(message string, tokenSecret string) (string, error) { + key := escape(s.consumerSecret) + "&" + escape(tokenSecret) + if s.debug { + fmt.Println("Signing:", message) + fmt.Println("Key:", key) + } + + h := hmac.New(s.HashFunc().New, []byte(key)) + h.Write([]byte(message)) + rawSignature := h.Sum(nil) + + base64signature := base64.StdEncoding.EncodeToString(rawSignature) + if s.debug { + fmt.Println("Base64 signature:", base64signature) + } + return base64signature, nil +} + +func (s *HMACSigner) Verify(message string, signature string) error { + if s.debug { + fmt.Println("Verifying Base64 signature:", signature) + } + validSignature, err := s.Sign(message, "") + if err != nil { + return err + } + + if validSignature != signature { + decodedSigniture, _ := url.QueryUnescape(signature) + if validSignature != decodedSigniture { + return fmt.Errorf("signature did not match") + } + } + + return nil +} + +func (s *HMACSigner) SignatureMethod() string { + return SIGNATURE_METHOD_HMAC + HASH_METHOD_MAP[s.HashFunc()] +} + +func (s *HMACSigner) HashFunc() crypto.Hash { + return s.hashFunc +} + +type RSASigner struct { + debug bool + rand io.Reader + privateKey *rsa.PrivateKey + hashFunc crypto.Hash +} + +func (s *RSASigner) Debug(enabled bool) { + s.debug = enabled +} + +func (s *RSASigner) Sign(message string, tokenSecret string) (string, error) { + if s.debug { + fmt.Println("Signing:", message) + } + + h := s.HashFunc().New() + h.Write([]byte(message)) + digest := h.Sum(nil) + + signature, err := rsa.SignPKCS1v15(s.rand, s.privateKey, s.HashFunc(), digest) + if err != nil { + return "", nil + } + + base64signature := base64.StdEncoding.EncodeToString(signature) + if s.debug { + fmt.Println("Base64 signature:", base64signature) + } + + return base64signature, nil +} + +func (s *RSASigner) Verify(message string, base64signature string) error { + if s.debug { + fmt.Println("Verifying:", message) + fmt.Println("Verifying Base64 signature:", base64signature) + } + + h := s.HashFunc().New() + h.Write([]byte(message)) + digest := h.Sum(nil) + + signature, err := base64.StdEncoding.DecodeString(base64signature) + if err != nil { + return err + } + + return rsa.VerifyPKCS1v15(&s.privateKey.PublicKey, s.HashFunc(), digest, signature) +} + +func (s *RSASigner) SignatureMethod() string { + return SIGNATURE_METHOD_RSA + HASH_METHOD_MAP[s.HashFunc()] +} + +func (s *RSASigner) HashFunc() crypto.Hash { + return s.hashFunc +} + +func escape(s string) string { + t := make([]byte, 0, 3*len(s)) + for i := 0; i < len(s); i++ { + c := s[i] + if isEscapable(c) { + t = append(t, '%') + t = append(t, "0123456789ABCDEF"[c>>4]) + t = append(t, "0123456789ABCDEF"[c&15]) + } else { + t = append(t, s[i]) + } + } + return string(t) +} + +func isEscapable(b byte) bool { + return !('A' <= b && b <= 'Z' || 'a' <= b && b <= 'z' || '0' <= b && b <= '9' || b == '-' || b == '.' || b == '_' || b == '~') + +} + +func (c *Consumer) requestString(method string, url string, params *OrderedParams) string { + result := method + "&" + escape(url) + for pos, key := range params.Keys() { + for innerPos, value := range params.Get(key) { + if pos+innerPos == 0 { + result += "&" + } else { + result += escape("&") + } + result += escape(fmt.Sprintf("%s=%s", key, value)) + } + } + return result +} + +func (c *Consumer) getBody(method, url string, oauthParams *OrderedParams) (*string, error) { + resp, err := c.httpExecute(method, url, "", 0, nil, oauthParams) + if err != nil { + return nil, errors.New("httpExecute: " + err.Error()) + } + bodyBytes, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return nil, errors.New("ReadAll: " + err.Error()) + } + bodyStr := string(bodyBytes) + if c.debug { + fmt.Printf("STATUS: %d %s\n", resp.StatusCode, resp.Status) + fmt.Println("BODY RESPONSE: " + bodyStr) + } + return &bodyStr, nil +} + +// HTTPExecuteError signals that a call to httpExecute failed. +type HTTPExecuteError struct { + // RequestHeaders provides a stringified listing of request headers. + RequestHeaders string + // ResponseBodyBytes is the response read into a byte slice. + ResponseBodyBytes []byte + // Status is the status code string response. + Status string + // StatusCode is the parsed status code. + StatusCode int +} + +// Error provides a printable string description of an HTTPExecuteError. +func (e HTTPExecuteError) Error() string { + return "HTTP response is not 200/OK as expected. Actual response: \n" + + "\tResponse Status: '" + e.Status + "'\n" + + "\tResponse Code: " + strconv.Itoa(e.StatusCode) + "\n" + + "\tResponse Body: " + string(e.ResponseBodyBytes) + "\n" + + "\tRequest Headers: " + e.RequestHeaders +} + +func (c *Consumer) httpExecute( + method string, urlStr string, contentType string, contentLength int, body io.Reader, oauthParams *OrderedParams) (*http.Response, error) { + // Create base request. + req, err := http.NewRequest(method, urlStr, body) + if err != nil { + return nil, errors.New("NewRequest failed: " + err.Error()) + } + + // Set auth header. + req.Header = http.Header{} + oauthHdr := "OAuth " + for pos, key := range oauthParams.Keys() { + for innerPos, value := range oauthParams.Get(key) { + if pos+innerPos > 0 { + oauthHdr += "," + } + oauthHdr += key + "=\"" + value + "\"" + } + } + req.Header.Add("Authorization", oauthHdr) + + // Add additional custom headers + for key, vals := range c.AdditionalHeaders { + for _, val := range vals { + req.Header.Add(key, val) + } + } + + // Set contentType if passed. + if contentType != "" { + req.Header.Set("Content-Type", contentType) + } + + // Set contentLength if passed. + if contentLength > 0 { + req.Header.Set("Content-Length", strconv.Itoa(contentLength)) + } + + if c.debug { + fmt.Printf("Request: %v\n", req) + } + resp, err := c.HttpClient.Do(req) + if err != nil { + return nil, errors.New("Do: " + err.Error()) + } + + debugHeader := "" + for k, vals := range req.Header { + for _, val := range vals { + debugHeader += "[key: " + k + ", val: " + val + "]" + } + } + + // StatusMultipleChoices is 300, any 2xx response should be treated as success + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + defer resp.Body.Close() + bytes, _ := ioutil.ReadAll(resp.Body) + + return resp, HTTPExecuteError{ + RequestHeaders: debugHeader, + ResponseBodyBytes: bytes, + Status: resp.Status, + StatusCode: resp.StatusCode, + } + } + return resp, err +} + +// +// String Sorting helpers +// + +type ByValue []string + +func (a ByValue) Len() int { + return len(a) +} + +func (a ByValue) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a ByValue) Less(i, j int) bool { + return a[i] < a[j] +} + +// +// ORDERED PARAMS +// + +type OrderedParams struct { + allParams map[string][]string + keyOrdering []string +} + +func NewOrderedParams() *OrderedParams { + return &OrderedParams{ + allParams: make(map[string][]string), + keyOrdering: make([]string, 0), + } +} + +func (o *OrderedParams) Get(key string) []string { + sort.Sort(ByValue(o.allParams[key])) + return o.allParams[key] +} + +func (o *OrderedParams) Keys() []string { + sort.Sort(o) + return o.keyOrdering +} + +func (o *OrderedParams) Add(key, value string) { + o.AddUnescaped(key, escape(value)) +} + +func (o *OrderedParams) AddUnescaped(key, value string) { + if _, exists := o.allParams[key]; !exists { + o.keyOrdering = append(o.keyOrdering, key) + o.allParams[key] = make([]string, 1) + o.allParams[key][0] = value + } else { + o.allParams[key] = append(o.allParams[key], value) + } +} + +func (o *OrderedParams) Len() int { + return len(o.keyOrdering) +} + +func (o *OrderedParams) Less(i int, j int) bool { + return o.keyOrdering[i] < o.keyOrdering[j] +} + +func (o *OrderedParams) Swap(i int, j int) { + o.keyOrdering[i], o.keyOrdering[j] = o.keyOrdering[j], o.keyOrdering[i] +} + +func (o *OrderedParams) Clone() *OrderedParams { + clone := NewOrderedParams() + for _, key := range o.Keys() { + for _, value := range o.Get(key) { + clone.AddUnescaped(key, value) + } + } + return clone +} diff --git a/vendor/github.com/mrjones/oauth/pre-commit.sh b/vendor/github.com/mrjones/oauth/pre-commit.sh new file mode 100644 index 0000000000000..91b9e88236074 --- /dev/null +++ b/vendor/github.com/mrjones/oauth/pre-commit.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# ln -s $PWD/pre-commit.sh .git/hooks/pre-commit +go test *.go +RESULT=$? +if [[ $RESULT != 0 ]]; then + echo "REJECTING COMMIT (test failed with status: $RESULT)" + exit 1; +fi + +go fmt *.go +for e in $(ls examples); do + go build examples/$e/*.go + RESULT=$? + if [[ $RESULT != 0 ]]; then + echo "REJECTING COMMIT (Examples failed to compile)" + exit $RESULT; + fi + go fmt examples/$e/*.go +done + +exit 0 diff --git a/vendor/github.com/mrjones/oauth/provider.go b/vendor/github.com/mrjones/oauth/provider.go new file mode 100644 index 0000000000000..da72fba3afec4 --- /dev/null +++ b/vendor/github.com/mrjones/oauth/provider.go @@ -0,0 +1,156 @@ +package oauth + +import ( + "bytes" + "fmt" + "math" + "net/http" + "net/url" + "strconv" + "strings" +) + +// +// OAuth1 2-legged provider +// Contributed by https://github.com/jacobpgallagher +// + +// Provide an buffer reader which implements the Close() interface +type oauthBufferReader struct { + *bytes.Buffer +} + +// So that it implements the io.ReadCloser interface +func (m oauthBufferReader) Close() error { return nil } + +type ConsumerGetter func(key string, header map[string]string) (*Consumer, error) + +// Provider provides methods for a 2-legged Oauth1 provider +type Provider struct { + ConsumerGetter ConsumerGetter + + // For mocking + clock clock +} + +// NewProvider takes a function to get the consumer secret from a datastore. +// Returns a Provider +func NewProvider(secretGetter ConsumerGetter) *Provider { + provider := &Provider{ + secretGetter, + &defaultClock{}, + } + return provider +} + +// Combine a URL and Request to make the URL absolute +func makeURLAbs(url *url.URL, request *http.Request) { + if !url.IsAbs() { + url.Host = request.Host + if request.TLS != nil || request.Header.Get("X-Forwarded-Proto") == "https" { + url.Scheme = "https" + } else { + url.Scheme = "http" + } + } +} + +// IsAuthorized takes an *http.Request and returns a pointer to a string containing the consumer key, +// or nil if not authorized +func (provider *Provider) IsAuthorized(request *http.Request) (*string, error) { + var err error + var userParams map[string]string + + // start with the body/query params + userParams, err = parseBody(request) + if err != nil { + return nil, err + } + + // if the oauth params are in the Authorization header, grab them, and + // let them override what's in userParams + authHeader := request.Header.Get(HTTP_AUTH_HEADER) + if len(authHeader) > 6 && strings.EqualFold(OAUTH_HEADER, authHeader[0:6]) { + authHeader = authHeader[6:] + params := strings.Split(authHeader, ",") + for _, param := range params { + vals := strings.SplitN(param, "=", 2) + k := strings.Trim(vals[0], " ") + v := strings.Trim(strings.Trim(vals[1], "\""), " ") + if strings.HasPrefix(k, "oauth") { + userParams[k], err = url.QueryUnescape(v) + if err != nil { + return nil, err + } + } + } + } + + // pop the request's signature, it's not included in our signature + // calculation + oauthSignature, ok := userParams[SIGNATURE_PARAM] + if !ok { + return nil, fmt.Errorf("no oauth signature") + } + delete(userParams, SIGNATURE_PARAM) + + // get the oauth consumer key + consumerKey, ok := userParams[CONSUMER_KEY_PARAM] + if !ok || consumerKey == "" { + return nil, fmt.Errorf("no consumer key") + } + + // use it to create a consumer object + consumer, err := provider.ConsumerGetter(consumerKey, userParams) + if err != nil { + return nil, err + } + + // Make sure timestamp is no more than 10 digits + timestamp := userParams[TIMESTAMP_PARAM] + if len(timestamp) > 10 { + timestamp = timestamp[0:10] + } + + // Check the timestamp + if !consumer.serviceProvider.IgnoreTimestamp { + oauthTimeNumber, err := strconv.Atoi(timestamp) + if err != nil { + return nil, err + } + + if math.Abs(float64(int64(oauthTimeNumber)-provider.clock.Seconds())) > 5*60 { + return nil, fmt.Errorf("too much clock skew") + } + } + + // if our consumer supports bodyhash, check it + if consumer.serviceProvider.BodyHash { + bodyHash, err := calculateBodyHash(request, consumer.signer) + if err != nil { + return nil, err + } + + sentHash, ok := userParams[BODY_HASH_PARAM] + + if bodyHash == "" && ok { + return nil, fmt.Errorf("body_hash must not be set") + } else if sentHash != bodyHash { + return nil, fmt.Errorf("body_hash mismatch") + } + } + + allParams := NewOrderedParams() + for key, value := range userParams { + allParams.Add(key, value) + } + + makeURLAbs(request.URL, request) + baseString := consumer.requestString(request.Method, canonicalizeUrl(request.URL), allParams) + err = consumer.signer.Verify(baseString, oauthSignature) + if err != nil { + return nil, err + } + + return &consumerKey, nil +} diff --git a/vendor/vendor.json b/vendor/vendor.json index bb363518ee760..c10df666f5277 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -586,6 +586,12 @@ "revision": "a0a751ca505adde67bc2c92e2aae99531b1e3213", "revisionTime": "2017-02-20T13:56:36Z" }, + { + "checksumSHA1": "1w0V6jYXaGlEtZcMeYTOAAucvgw=", + "path": "github.com/markbates/goth/providers/twitter", + "revision": "7276be0fdf719ddff753f3574ef0f967e4a5a5f7", + "revisionTime": "2017-02-20T14:02:47Z" + }, { "checksumSHA1": "9FJUwn3EIgASVki+p8IHgWVC5vQ=", "path": "github.com/mattn/go-sqlite3", @@ -604,6 +610,12 @@ "revision": "f77f16ffc87a6a58814e64ae72d55f9c41374e6d", "revisionTime": "2016-10-12T08:37:05Z" }, + { + "checksumSHA1": "/le/3bmROVfXmIQT9CbS22aqdcE=", + "path": "github.com/mrjones/oauth", + "revision": "1359071b7221e8f5ffa3985727f51336e16f741a", + "revisionTime": "2017-01-08T19:16:49Z" + }, { "checksumSHA1": "lfOuMiAdiqc/dalUSBTvD5ZMSzA=", "path": "github.com/msteinert/pam", From 92aec118ee76716967453d1db5e9db6822b48c5e Mon Sep 17 00:00:00 2001 From: willemvd Date: Tue, 21 Feb 2017 15:26:55 +0100 Subject: [PATCH 07/19] add facebook provider --- models/login_source.go | 1 + modules/auth/oauth2/oauth2.go | 3 + options/locale/locale_en-US.ini | 1 + public/img/auth/facebook.png | Bin 0 -> 2327 bytes templates/admin/auth/new.tmpl | 4 +- .../goth/providers/facebook/facebook.go | 195 ++++++++++++++++++ .../goth/providers/facebook/session.go | 59 ++++++ vendor/vendor.json | 6 + 8 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 public/img/auth/facebook.png create mode 100644 vendor/github.com/markbates/goth/providers/facebook/facebook.go create mode 100644 vendor/github.com/markbates/goth/providers/facebook/session.go diff --git a/models/login_source.go b/models/login_source.go index b1d2f703528ea..d8951f4e74e5e 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -600,6 +600,7 @@ type OAuth2Provider struct { // value is used to store display data var OAuth2Providers = map[string]OAuth2Provider{ "bitbucket": {Name: "bitbucket", DisplayName: "Bitbucket", Image: "/img/auth/bitbucket.png"}, + "facebook": {Name: "facebook", DisplayName: "Facebook", Image: "/img/auth/facebook.png"}, "github": {Name: "github", DisplayName: "GitHub", Image: "/img/auth/github.png"}, "gitlab": {Name: "gitlab", DisplayName: "GitLab", Image: "/img/auth/gitlab.png"}, "gplus": {Name: "gplus", DisplayName: "Google+", Image: "/img/auth/google_plus.png"}, diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index 5832a0da5434c..2e177c4f343c4 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -19,6 +19,7 @@ import ( "github.com/markbates/goth/providers/gitlab" "github.com/markbates/goth/providers/bitbucket" "github.com/markbates/goth/providers/twitter" + "github.com/markbates/goth/providers/facebook" ) var ( @@ -98,6 +99,8 @@ func createProvider(providerName, providerType, clientID, clientSecret string) g switch providerType { case "bitbucket": provider = bitbucket.New(clientID, clientSecret, callbackURL, "account") + case "facebook": + provider = facebook.New(clientID, clientSecret, callbackURL, "email") case "github": provider = github.New(clientID, clientSecret, callbackURL, "user:email") case "gitlab": diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 45c34d87d25e2..8551e9b8d607d 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1125,6 +1125,7 @@ auths.tips = Tips auths.tips.oauth2.general = OAuth2 General auths.tips.oauth2.general.tip = When registering a new OAuth2 application, the callback/redirect URL should be: /user/oauth2//callback auths.tip.bitbucket = Register a new OAuth consumer on https://bitbucket.org/account/user//oauth-consumers/new and add the permission "Account"-"Read" +auths.tip.facebook = Register a new App on https://developers.facebook.com/apps and add the product "Facebook Login" auths.tip.github = Register a new OAuth application on https://github.com/settings/applications/new auths.tip.gitlab = Register a new application on https://gitlab.com/profile/applications auths.tip.google_plus = Obtain OAuth2 client credentials from the Google API console (https://console.developers.google.com/) diff --git a/public/img/auth/facebook.png b/public/img/auth/facebook.png new file mode 100644 index 0000000000000000000000000000000000000000..29273af14fb019078c13b44175efaf59fed6ea1b GIT binary patch literal 2327 zcmXX|dpwkB8-5L?D6*y0VrDz+r-Q_thYXbrUs0k4b23A02a0Mxq8U-CR+BLAJH#kT zTMGTKQ5dP5&E_;x8zP5j(Kroe=6j~y`#1B<^W67+UC(vh@B7DGJ>cQG++dvn1VPK) zXnVZCuR!}le+Ursy$Oo=BALjw~0q!A{(Z#5L;e4iID=ZL}n@>7{;^GNF0R7N+mF0 z3OkL=6cO1Xz)ztg6efZT5)%W&07zgVWHw??*KX_pIY@F~d_Wq32@{zJ#aBdN!DJ@r z6yZ4t={u0X)b5E4n7|PenJ~#02tWt{2v`9vAwW!Gz(6V};kgLe55a#gkyr?pC&mYe z@jSq$1!92)ju;z=5ZGe;cdV92`_U3o0FWcb1;XHnX8|KXB$xn#4b<}6`+*xl8`vXu zJcQ(j-~wQ9yOy7;<=4vi2jvUf@n8xa2B`1MA_4;;FcBag^ZQ#rpdC zOAHJRmn}CkGG4L5C^+;~#F=yFBF{%h$HedjATC|Ic0KXtt>jb@f{4@8 zGcqK1v$J#V-G7vqmtO$lY4Nkt=g%voRj)w2epBDj*d%LdX>DukkaxX*|5p!)zP|qc z!J(nyFC!zPqvI3bz9|%n$;rv7nb}$8{KA4trPhX!+3r<&HsqG z{`gp;SZuw^|1qE2Vp}W@y7$!}(K^F8-D>p=X0~T>T!nj3T$N>s5kb^ao+r#UEg>(y zcW$Mj*TJ$oD(qn1l&qzwsPQh%Ah84KYTA)(HqEK}a9&0x4}LuV#JC~SBtlj$87p;b zR%ZN&k%wE$)nog6M@mlSHf%lOcM)E&aDCvqp@$Zv$Q}jJv9z#o$j{N;3e9;2~ zT>W@#IljvD2ILb<{5&hjJsP#?xF9^zvFFL$hRjXU1~#Eyq2pOMg|nCbo@^ImRnex0 zb*lC)bv!VC%A@`DolFfSaNg;>NuNni)&?0dqJ$8=BXW7$6KWa|OBq;^(y z>GhyYZg>7L%X_N3eO-Kj#Is59Cp9%A(=WUrrP2@Q5Z!T`+&eY1m(i)uD1$tzNBDmVDu*fi-En(xH~*4_ z(*yO!Hcn#f*B2E>*_<48;&Ag!bnRF&YDq`;@iIePW&PMnR6>VlDO2?p(=(#GWQ^+7 zwbi`yKRqKTvv-=1H2+uf#Rbxub4FCRrJPGblnL8HI;ma>jo(VhH{c}N;4b(GtS7Lj z6N#Sr`X{~-bx$ODTGQz2$CX%(e5WRn)jBE5&z>)W8YeWQ`fZhSb2yt9KDi3hf$-ZZ z_4Q(d%brG@$HHTmJ(qC`g>o~;&4MJQr2WJETF0LR?T*pg*l1}upZ7kaKTp50iP~QR ze?G2D8}H>Q(;&O(nG2$PBTV%b;qW6AZKC(+oLHxS_Q6p;-~4GzTR~;z+|^ocH?umE zYJN+#MuCY@htsCnqsh-C#;26%;$TaAZ1(Vnu3M4n&`(odDNuA@cP=F}-y)*dxnnMW zqCG6OXDqD0$rkhJ5sKcw`;}~2!KMp)p|}cebFN&MplpkhZkHT3?i>hPOzRxUb<^4Q ztqQBgGX==w3*l~SFpk05!rC(RZnKiNHM~GWj9exq=r(o@@wPWu-2A#V$9{)Jp^SZd zy&m17xT%JI5og+6*i@r;+Im)Xb(7=M`L`xqL2lgMXqOK)QiDjmc}bn8(LqsP9m_v@ z@=C~*G!?N>A43;o#9s=lTz^eqnJ~+2x+SHJR&}eJuh0KtOO5>L%@+47&DZDLY|%{O zyCCHsap5XUMQm0J>i}_;NotxL$a;x@xBD+}g?39A)Kv__Fmn zD^vr2bR4#nHost: smtp.gmail.com, Port: 587, Enable TLS Encryption: true

{{.i18n.Tr "admin.auths.tips.oauth2.general"}}:

{{.i18n.Tr "admin.auths.tips.oauth2.general.tip"}}

-
OAuth Bitbucket:
+
OAuth2 Bitbucket:

{{.i18n.Tr "admin.auths.tip.bitbucket"}}

+
OAuth2 Facebook:
+

{{.i18n.Tr "admin.auths.tip.facebook"}}

OAuth GitHub:

{{.i18n.Tr "admin.auths.tip.github"}}

OAuth2 GitLab:
diff --git a/vendor/github.com/markbates/goth/providers/facebook/facebook.go b/vendor/github.com/markbates/goth/providers/facebook/facebook.go new file mode 100644 index 0000000000000..e0cfdf1e34a0b --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/facebook/facebook.go @@ -0,0 +1,195 @@ +// Package facebook implements the OAuth2 protocol for authenticating users through Facebook. +// This package can be used as a reference implementation of an OAuth2 provider for Goth. +package facebook + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "io/ioutil" + "net/http" + "net/url" + + "github.com/markbates/goth" + "golang.org/x/oauth2" + "fmt" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" +) + +const ( + authURL string = "https://www.facebook.com/dialog/oauth" + tokenURL string = "https://graph.facebook.com/oauth/access_token" + endpointProfile string = "https://graph.facebook.com/me?fields=email,first_name,last_name,link,about,id,name,picture,location" +) + +// New creates a new Facebook provider, and sets up important connection details. +// You should always call `facebook.New` to get a new Provider. Never try to create +// one manually. +func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { + p := &Provider{ + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + providerName: "facebook", + } + p.config = newConfig(p, scopes) + return p +} + +// Provider is the implementation of `goth.Provider` for accessing Facebook. +type Provider struct { + ClientKey string + Secret string + CallbackURL string + HTTPClient *http.Client + config *oauth2.Config + providerName string +} + +// Name is the name used to retrieve this provider later. +func (p *Provider) Name() string { + return p.providerName +} + +// SetName is to update the name of the provider (needed in case of multiple providers of 1 type) +func (p *Provider) SetName(name string) { + p.providerName = name +} + +func (p *Provider) Client() *http.Client { + return goth.HTTPClientWithFallBack(p.HTTPClient) +} + +// Debug is a no-op for the facebook package. +func (p *Provider) Debug(debug bool) {} + +// BeginAuth asks Facebook for an authentication end-point. +func (p *Provider) BeginAuth(state string) (goth.Session, error) { + url := p.config.AuthCodeURL(state) + session := &Session{ + AuthURL: url, + } + return session, nil +} + +// FetchUser will go to Facebook and access basic information about the user. +func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { + sess := session.(*Session) + user := goth.User{ + AccessToken: sess.AccessToken, + Provider: p.Name(), + ExpiresAt: sess.ExpiresAt, + } + + if user.AccessToken == "" { + // data is not yet retrieved since accessToken is still empty + return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) + } + + // always add appsecretProof to make calls more protected + // https://github.com/markbates/goth/issues/96 + // https://developers.facebook.com/docs/graph-api/securing-requests + hash := hmac.New(sha256.New, []byte(p.Secret)) + hash.Write([]byte(sess.AccessToken)) + appsecretProof := hex.EncodeToString(hash.Sum(nil)) + + response, err := p.Client().Get(endpointProfile + "&access_token=" + url.QueryEscape(sess.AccessToken) + "&appsecret_proof=" + appsecretProof) + if err != nil { + return user, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode) + } + + bits, err := ioutil.ReadAll(response.Body) + if err != nil { + return user, err + } + + err = json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData) + if err != nil { + return user, err + } + + err = userFromReader(bytes.NewReader(bits), &user) + return user, err +} + +func userFromReader(reader io.Reader, user *goth.User) error { + u := struct { + ID string `json:"id"` + Email string `json:"email"` + About string `json:"about"` + Name string `json:"name"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Link string `json:"link"` + Picture struct { + Data struct { + URL string `json:"url"` + } `json:"data"` + } `json:"picture"` + Location struct { + Name string `json:"name"` + } `json:"location"` + }{} + + err := json.NewDecoder(reader).Decode(&u) + if err != nil { + return err + } + + user.Name = u.Name + user.FirstName = u.FirstName + user.LastName = u.LastName + user.NickName = u.Name + user.Email = u.Email + user.Description = u.About + user.AvatarURL = u.Picture.Data.URL + user.UserID = u.ID + user.Location = u.Location.Name + + return err +} + +func newConfig(provider *Provider, scopes []string) *oauth2.Config { + c := &oauth2.Config{ + ClientID: provider.ClientKey, + ClientSecret: provider.Secret, + RedirectURL: provider.CallbackURL, + Endpoint: oauth2.Endpoint{ + AuthURL: authURL, + TokenURL: tokenURL, + }, + Scopes: []string{ + "email", + }, + } + + defaultScopes := map[string]struct{}{ + "email": {}, + } + + for _, scope := range scopes { + if _, exists := defaultScopes[scope]; !exists { + c.Scopes = append(c.Scopes, scope) + } + } + + return c +} + +//RefreshToken refresh token is not provided by facebook +func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + return nil, errors.New("Refresh token is not provided by facebook") +} + +//RefreshTokenAvailable refresh token is not provided by facebook +func (p *Provider) RefreshTokenAvailable() bool { + return false +} diff --git a/vendor/github.com/markbates/goth/providers/facebook/session.go b/vendor/github.com/markbates/goth/providers/facebook/session.go new file mode 100644 index 0000000000000..5cdcca443acaa --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/facebook/session.go @@ -0,0 +1,59 @@ +package facebook + +import ( + "encoding/json" + "errors" + "strings" + "time" + + "github.com/markbates/goth" +) + +// Session stores data during the auth process with Facebook. +type Session struct { + AuthURL string + AccessToken string + ExpiresAt time.Time +} + +// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Facebook provider. +func (s Session) GetAuthURL() (string, error) { + if s.AuthURL == "" { + return "", errors.New(goth.NoAuthUrlErrorMessage) + } + return s.AuthURL, nil +} + +// Authorize the session with Facebook and return the access token to be stored for future use. +func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { + p := provider.(*Provider) + token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code")) + if err != nil { + return "", err + } + + if !token.Valid() { + return "", errors.New("Invalid token received from provider") + } + + s.AccessToken = token.AccessToken + s.ExpiresAt = token.Expiry + return token.AccessToken, err +} + +// Marshal the session into a string +func (s Session) Marshal() string { + b, _ := json.Marshal(s) + return string(b) +} + +func (s Session) String() string { + return s.Marshal() +} + +// UnmarshalSession will unmarshal a JSON string into a session. +func (p *Provider) UnmarshalSession(data string) (goth.Session, error) { + sess := &Session{} + err := json.NewDecoder(strings.NewReader(data)).Decode(sess) + return sess, err +} diff --git a/vendor/vendor.json b/vendor/vendor.json index c10df666f5277..93ed93b4c1a46 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -568,6 +568,12 @@ "revision": "7276be0fdf719ddff753f3574ef0f967e4a5a5f7", "revisionTime": "2017-02-20T14:02:47Z" }, + { + "checksumSHA1": "cGs1da29iOBJh5EAH0icKDbN8CA=", + "path": "github.com/markbates/goth/providers/facebook", + "revision": "7276be0fdf719ddff753f3574ef0f967e4a5a5f7", + "revisionTime": "2017-02-20T14:02:47Z" + }, { "checksumSHA1": "ZFqznX3/ZW65I4QeepiHQdE69nA=", "path": "github.com/markbates/goth/providers/github", From 9e797b2b2053b3146e72a1fb5a3ca7e9decff8c9 Mon Sep 17 00:00:00 2001 From: willemvd Date: Tue, 21 Feb 2017 15:44:52 +0100 Subject: [PATCH 08/19] add dropbox provider --- models/login_source.go | 1 + modules/auth/oauth2/oauth2.go | 3 + options/locale/locale_en-US.ini | 1 + public/img/auth/dropbox.png | Bin 0 -> 1427 bytes templates/admin/auth/new.tmpl | 2 + .../goth/providers/dropbox/dropbox.go | 191 ++++++++++++++++++ vendor/vendor.json | 6 + 7 files changed, 204 insertions(+) create mode 100644 public/img/auth/dropbox.png create mode 100644 vendor/github.com/markbates/goth/providers/dropbox/dropbox.go diff --git a/models/login_source.go b/models/login_source.go index d8951f4e74e5e..08c066a5ba623 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -600,6 +600,7 @@ type OAuth2Provider struct { // value is used to store display data var OAuth2Providers = map[string]OAuth2Provider{ "bitbucket": {Name: "bitbucket", DisplayName: "Bitbucket", Image: "/img/auth/bitbucket.png"}, + "dropbox": {Name: "dropbox", DisplayName: "Dropbox", Image: "/img/auth/dropbox.png"}, "facebook": {Name: "facebook", DisplayName: "Facebook", Image: "/img/auth/facebook.png"}, "github": {Name: "github", DisplayName: "GitHub", Image: "/img/auth/github.png"}, "gitlab": {Name: "gitlab", DisplayName: "GitLab", Image: "/img/auth/gitlab.png"}, diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index 2e177c4f343c4..e529ce6bc5fc5 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -20,6 +20,7 @@ import ( "github.com/markbates/goth/providers/bitbucket" "github.com/markbates/goth/providers/twitter" "github.com/markbates/goth/providers/facebook" + "github.com/markbates/goth/providers/dropbox" ) var ( @@ -99,6 +100,8 @@ func createProvider(providerName, providerType, clientID, clientSecret string) g switch providerType { case "bitbucket": provider = bitbucket.New(clientID, clientSecret, callbackURL, "account") + case "dropbox": + provider = dropbox.New(clientID, clientSecret, callbackURL) case "facebook": provider = facebook.New(clientID, clientSecret, callbackURL, "email") case "github": diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 8551e9b8d607d..0d62697f38286 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1125,6 +1125,7 @@ auths.tips = Tips auths.tips.oauth2.general = OAuth2 General auths.tips.oauth2.general.tip = When registering a new OAuth2 application, the callback/redirect URL should be: /user/oauth2//callback auths.tip.bitbucket = Register a new OAuth consumer on https://bitbucket.org/account/user//oauth-consumers/new and add the permission "Account"-"Read" +auths.tip.dropbox = Create a new App on https://www.dropbox.com/developers/apps auths.tip.facebook = Register a new App on https://developers.facebook.com/apps and add the product "Facebook Login" auths.tip.github = Register a new OAuth application on https://github.com/settings/applications/new auths.tip.gitlab = Register a new application on https://gitlab.com/profile/applications diff --git a/public/img/auth/dropbox.png b/public/img/auth/dropbox.png new file mode 100644 index 0000000000000000000000000000000000000000..da2348872a06bd71a68b900e3ae538f613147db3 GIT binary patch literal 1427 zcmV;E1#J3>P)?rUF31=AkjtGC6BzyA4YBbU zc}+ekcOf>sFc862CnrA+LwxZ8jZGN4Hr_ydaT3K;BqkI_LYy$#!0X&_h!Z9tyj965 zi*pbcEJFmHCuRY}1y@kn%)~^&RESRJk=ljv6XOI#r@4q>baHCoCd44mQQV2~3*aHd zAa_y6h{PnoGKfjqIuJusctK3C21#8>%s*(0mk^W9LBJox@Gqu7O!fxd@Smf}NrjCN z3vESoO$-sS24bNd$lzOI0-z7xLln4&e8_Y2VgpUvt9YsMGN{`$YTqnKI+OI%z;Worzuh1ERbrh}v(9Wd#>;Vcn8c1|(hZtlL z5*gein+-9@e#CL*Y>k2Fa2=&goIn@GMTibl5kyZAHC?zaeMB#WvY`DE44v@~;)D;V zV^k11a2?`+T}Y{*iI)UhoS4n$Gdh4sSmDRVsVe?f+y*X(%Md#@B8HjEIwK$&UYLMz zZnTgP24mm>(U^$;tO%ey?qDM_yYV_*I;_GyRJVHY*{JdpEM}I(eEenAfYEF&hO-dE z4HFX?li>})Uc@nb&^)+drwVNW|89_&f6xr~?ev_Am^E-?2kN1H$e}Pf8L<=M3s1B* zx)7TNagZm*BC3UniGblw7PPYvT<^efuovQj+o)$ja;o7v#05u?NYB;(E<~sG$Y?@h z(qI)tr-!KM?BH$S12M=)3_@6giokw&2Qg4r{H=fOTc2yFqH|xsC5TB@AcDQYlUdlC zUWpV=uFEfiSl}$u+6GVx&mb21fSw5EOJV|{6W&5B^a_==*Bepj6pGr}uy6#TP}k3a zWWZ*KVvCRz2{9X@*iK~Cp0S2_2vKMx20#>gh8FnyN1yo^SOig^AO89hM^}gfD-qAp z>huQ00`E{qyY1!6-&ODmVu8D;=gRzM27DkUxryAGIo|?oMdU9(%Zw`!6U|2yJ(X)d zT!a{G7ouv`xsF16z^Hjoa16%>1J7E(FYlcd_aHW2qm-t{Gu^;F zK;sbFpGgzM!w?sFql4!C>N}A1UYg^Z!vo@?lcx938Nfe!;f)JBTgz)!M}|TiG!H>^ zRQ>NU2!X8Z-X+FPhyyMmwPrTH2I8cHh^bj5Jnh7~drDO;&cDzak0Ew!LWD2JC)c?d z{1$R*IuEP-_!rw5|EfyPzc!u6)esvK@b{NvO5>$Ly`=`4u}45`tV0~lG5@JZP*Ti8 zOq+hq9eAUm=I7l_*bF{NQ&iT32Ogq|O@C`T3aUs@5(plnmgeVCkwLSCW-i1eTM$~a z@iUf9%lHZeMO7pyF+6N~&q*}DkA=MulbuIen-eq-qET2yf|5aOyhIb^z%7VHUZOP0 z+9^7Vt4L4;T!6T_2S}pof3i^MgEtU2J}yyBr2Xow>K%Bw_z~{SSB{Q!lg%xjp~@002ovPDHLkV1gOoi|POX literal 0 HcmV?d00001 diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl index d4ad00eb38262..123f46601a4dc 100644 --- a/templates/admin/auth/new.tmpl +++ b/templates/admin/auth/new.tmpl @@ -199,6 +199,8 @@

{{.i18n.Tr "admin.auths.tips.oauth2.general.tip"}}

OAuth2 Bitbucket:

{{.i18n.Tr "admin.auths.tip.bitbucket"}}

+
OAuth2 Dropbox:
+

{{.i18n.Tr "admin.auths.tip.dropbox"}}

OAuth2 Facebook:

{{.i18n.Tr "admin.auths.tip.facebook"}}

OAuth GitHub:
diff --git a/vendor/github.com/markbates/goth/providers/dropbox/dropbox.go b/vendor/github.com/markbates/goth/providers/dropbox/dropbox.go new file mode 100644 index 0000000000000..61533d405eb46 --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/dropbox/dropbox.go @@ -0,0 +1,191 @@ +// Package dropbox implements the OAuth2 protocol for authenticating users through Dropbox. +package dropbox + +import ( + "encoding/json" + "errors" + "io" + "net/http" + "strings" + + "github.com/markbates/goth" + "golang.org/x/oauth2" + "fmt" +) + +const ( + authURL = "https://www.dropbox.com/1/oauth2/authorize" + tokenURL = "https://api.dropbox.com/1/oauth2/token" + accountURL = "https://api.dropbox.com/1/account/info" +) + +// Provider is the implementation of `goth.Provider` for accessing Dropbox. +type Provider struct { + ClientKey string + Secret string + CallbackURL string + HTTPClient *http.Client + config *oauth2.Config + providerName string +} + +// Session stores data during the auth process with Dropbox. +type Session struct { + AuthURL string + Token string +} + +// New creates a new Dropbox provider and sets up important connection details. +// You should always call `dropbox.New` to get a new provider. Never try to +// create one manually. +func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { + p := &Provider{ + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + providerName: "dropbox", + } + p.config = newConfig(p, scopes) + return p +} + +// Name is the name used to retrieve this provider later. +func (p *Provider) Name() string { + return p.providerName +} + +// SetName is to update the name of the provider (needed in case of multiple providers of 1 type) +func (p *Provider) SetName(name string) { + p.providerName = name +} + +func (p *Provider) Client() *http.Client { + return goth.HTTPClientWithFallBack(p.HTTPClient) +} + +// Debug is a no-op for the dropbox package. +func (p *Provider) Debug(debug bool) {} + +// BeginAuth asks Dropbox for an authentication end-point. +func (p *Provider) BeginAuth(state string) (goth.Session, error) { + return &Session{ + AuthURL: p.config.AuthCodeURL(state), + }, nil +} + +// FetchUser will go to Dropbox and access basic information about the user. +func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { + s := session.(*Session) + user := goth.User{ + AccessToken: s.Token, + Provider: p.Name(), + } + + if user.AccessToken == "" { + // data is not yet retrieved since accessToken is still empty + return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) + } + + req, err := http.NewRequest("GET", accountURL, nil) + if err != nil { + return user, err + } + req.Header.Set("Authorization", "Bearer "+s.Token) + resp, err := p.Client().Do(req) + if err != nil { + return user, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, resp.StatusCode) + } + + err = userFromReader(resp.Body, &user) + return user, err +} + +// UnmarshalSession wil unmarshal a JSON string into a session. +func (p *Provider) UnmarshalSession(data string) (goth.Session, error) { + s := &Session{} + err := json.NewDecoder(strings.NewReader(data)).Decode(s) + return s, err +} + +// GetAuthURL gets the URL set by calling the `BeginAuth` function on the Dropbox provider. +func (s *Session) GetAuthURL() (string, error) { + if s.AuthURL == "" { + return "", errors.New("dropbox: missing AuthURL") + } + return s.AuthURL, nil +} + +// Authorize the session with Dropbox and return the access token to be stored for future use. +func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { + p := provider.(*Provider) + token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code")) + if err != nil { + return "", err + } + + if !token.Valid() { + return "", errors.New("Invalid token received from provider") + } + + s.Token = token.AccessToken + return token.AccessToken, nil +} + +// Marshal the session into a string +func (s *Session) Marshal() string { + b, _ := json.Marshal(s) + return string(b) +} + +func (s Session) String() string { + return s.Marshal() +} + +func newConfig(p *Provider, scopes []string) *oauth2.Config { + c := &oauth2.Config{ + ClientID: p.ClientKey, + ClientSecret: p.Secret, + RedirectURL: p.CallbackURL, + Endpoint: oauth2.Endpoint{ + AuthURL: authURL, + TokenURL: tokenURL, + }, + } + return c +} + +func userFromReader(r io.Reader, user *goth.User) error { + u := struct { + Name string `json:"display_name"` + NameDetails struct { + NickName string `json:"familiar_name"` + } `json:"name_details"` + Location string `json:"country"` + Email string `json:"email"` + }{} + err := json.NewDecoder(r).Decode(&u) + if err != nil { + return err + } + user.Email = u.Email + user.Name = u.Name + user.NickName = u.NameDetails.NickName + user.UserID = u.Email // Dropbox doesn't provide a separate user ID + user.Location = u.Location + return nil +} + +//RefreshToken refresh token is not provided by dropbox +func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + return nil, errors.New("Refresh token is not provided by dropbox") +} + +//RefreshTokenAvailable refresh token is not provided by dropbox +func (p *Provider) RefreshTokenAvailable() bool { + return false +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 93ed93b4c1a46..884a5fe33bde0 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -568,6 +568,12 @@ "revision": "7276be0fdf719ddff753f3574ef0f967e4a5a5f7", "revisionTime": "2017-02-20T14:02:47Z" }, + { + "checksumSHA1": "1Kp4DKkJNVn135Xg8H4a6CFBNy8=", + "path": "github.com/markbates/goth/providers/dropbox", + "revision": "7276be0fdf719ddff753f3574ef0f967e4a5a5f7", + "revisionTime": "2017-02-20T14:02:47Z" + }, { "checksumSHA1": "cGs1da29iOBJh5EAH0icKDbN8CA=", "path": "github.com/markbates/goth/providers/facebook", From 107eeea3fca752157b9546ff970ac401461b953c Mon Sep 17 00:00:00 2001 From: willemvd Date: Wed, 22 Feb 2017 13:11:24 +0100 Subject: [PATCH 09/19] add openid connect provider incl. new format of tips section in "Add New Source" --- models/error.go | 25 ++ models/login_source.go | 62 ++- modules/auth/auth_form.go | 57 +-- modules/auth/oauth2/oauth2.go | 31 +- options/locale/locale_en-US.ini | 9 +- public/css/index.css | 5 +- public/img/auth/openid_connect.png | Bin 0 -> 12921 bytes public/js/index.js | 18 +- routers/admin/auths.go | 14 +- templates/admin/auth/edit.tmpl | 4 + templates/admin/auth/new.tmpl | 39 +- templates/user/auth/signin_inner.tmpl | 2 +- .../providers/openidConnect/openidConnect.go | 384 ++++++++++++++++++ .../goth/providers/openidConnect/session.go | 63 +++ vendor/vendor.json | 6 + 15 files changed, 643 insertions(+), 76 deletions(-) create mode 100644 public/img/auth/openid_connect.png create mode 100644 vendor/github.com/markbates/goth/providers/openidConnect/openidConnect.go create mode 100644 vendor/github.com/markbates/goth/providers/openidConnect/session.go diff --git a/models/error.go b/models/error.go index 472c8c94266a5..7382c60256082 100644 --- a/models/error.go +++ b/models/error.go @@ -887,3 +887,28 @@ func IsErrExternalLoginUserNotExist(err error) bool { func (err ErrExternalLoginUserNotExist) Error() string { return fmt.Sprintf("external login user link does not exists [userID: %d, loginSourceID: %d]", err.UserID, err.LoginSourceID) } + +// ________ _____ ____ _________________ ___ +// \_____ \ / _ \ | | \__ ___/ | \ +// / | \ / /_\ \| | / | | / ~ \ +// / | \/ | \ | / | | \ Y / +// \_______ /\____|__ /______/ |____| \___|_ / +// \/ \/ \/ + + +// ErrOpenIDConnectInitialize represents a "OpenIDConnectInitialize" kind of error. +type ErrOpenIDConnectInitialize struct { + OpenIDConnectAutoDiscoveryURL string + ProviderName string + Cause error +} + +// IsErrOpenIDConnectInitialize checks if an error is a ExternalLoginUserAlreadyExist. +func IsErrOpenIDConnectInitialize(err error) bool { + _, ok := err.(ErrOpenIDConnectInitialize) + return ok +} + +func (err ErrOpenIDConnectInitialize) Error() string { + return fmt.Sprintf("Failed to initialize OpenID Connect Provider with name '%s' with url '%s': %v", err.ProviderName, err.OpenIDConnectAutoDiscoveryURL, err.Cause) +} \ No newline at end of file diff --git a/models/login_source.go b/models/login_source.go index 08c066a5ba623..4e76108574af9 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -122,9 +122,10 @@ func (cfg *PAMConfig) ToDB() ([]byte, error) { // OAuth2Config holds configuration for the OAuth2 login source. type OAuth2Config struct { - Provider string - ClientID string - ClientSecret string + Provider string + ClientID string + ClientSecret string + OpenIDConnectAutoDiscoveryURL string } // FromDB fills up an OAuth2Config from serialized format. @@ -295,9 +296,15 @@ func CreateLoginSource(source *LoginSource) error { } _, err = x.Insert(source) - if err == nil && source.IsOAuth2() { + if err == nil && source.IsOAuth2() && source.IsActived { oAuth2Config := source.OAuth2() - oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret) + err = oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL) + err = wrapOpenIDConnectInitializeError(err, source.Name, oAuth2Config) + + if err != nil { + // remove the LoginSource in case of errors while registering OAuth2 providers + x.Delete(source) + } } return err } @@ -322,11 +329,25 @@ func GetLoginSourceByID(id int64) (*LoginSource, error) { // UpdateSource updates a LoginSource record in DB. func UpdateSource(source *LoginSource) error { + var originalLoginSource *LoginSource + if source.IsOAuth2() { + // keep track of the original values so we can restore in case of errors while registering OAuth2 providers + var err error + if originalLoginSource, err = GetLoginSourceByID(source.ID); err != nil { + return err + } + } + _, err := x.Id(source.ID).AllCols().Update(source) - if err == nil && source.IsOAuth2() { + if err == nil && source.IsOAuth2() && source.IsActived { oAuth2Config := source.OAuth2() - oauth2.RemoveProvider(source.Name) - oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret) + err = oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL) + err = wrapOpenIDConnectInitializeError(err, source.Name, oAuth2Config) + + if err != nil { + // restore original values since we cannot update the provider it self + x.Id(source.ID).AllCols().Update(originalLoginSource) + } } return err } @@ -599,13 +620,14 @@ type OAuth2Provider struct { // key is used to map the OAuth2Provider with the goth provider type (also in LoginSource.OAuth2Config.Provider) // value is used to store display data var OAuth2Providers = map[string]OAuth2Provider{ - "bitbucket": {Name: "bitbucket", DisplayName: "Bitbucket", Image: "/img/auth/bitbucket.png"}, - "dropbox": {Name: "dropbox", DisplayName: "Dropbox", Image: "/img/auth/dropbox.png"}, - "facebook": {Name: "facebook", DisplayName: "Facebook", Image: "/img/auth/facebook.png"}, - "github": {Name: "github", DisplayName: "GitHub", Image: "/img/auth/github.png"}, - "gitlab": {Name: "gitlab", DisplayName: "GitLab", Image: "/img/auth/gitlab.png"}, - "gplus": {Name: "gplus", DisplayName: "Google+", Image: "/img/auth/google_plus.png"}, - "twitter": {Name: "twitter", DisplayName: "Twitter", Image: "/img/auth/twitter.png"}, + "bitbucket": {Name: "bitbucket", DisplayName: "Bitbucket", Image: "/img/auth/bitbucket.png"}, + "dropbox": {Name: "dropbox", DisplayName: "Dropbox", Image: "/img/auth/dropbox.png"}, + "facebook": {Name: "facebook", DisplayName: "Facebook", Image: "/img/auth/facebook.png"}, + "github": {Name: "github", DisplayName: "GitHub", Image: "/img/auth/github.png"}, + "gitlab": {Name: "gitlab", DisplayName: "GitLab", Image: "/img/auth/gitlab.png"}, + "gplus": {Name: "gplus", DisplayName: "Google+", Image: "/img/auth/google_plus.png"}, + "openidConnect": {Name: "openidConnect", DisplayName: "OpenID Connect", Image: "/img/auth/openid_connect.png"}, + "twitter": {Name: "twitter", DisplayName: "Twitter", Image: "/img/auth/twitter.png"}, } // ExternalUserLogin attempts a login using external source types. @@ -738,6 +760,14 @@ func InitOAuth2() { for _, source := range loginSources { oAuth2Config := source.OAuth2() - oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret) + oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL) } } +// wrapOpenIDConnectInitializeError is used to wrap the error but this cannot be done in modules/auth/oauth2 +// inside oauth2: import cycle not allowed models -> modules/auth/oauth2 -> models +func wrapOpenIDConnectInitializeError(err error, providerName string, oAuth2Config *OAuth2Config) error { + if err != nil && "openidConnect" == oAuth2Config.Provider { + err = ErrOpenIDConnectInitialize{ProviderName:providerName,OpenIDConnectAutoDiscoveryURL:oAuth2Config.OpenIDConnectAutoDiscoveryURL,Cause:err} + } + return err +} \ No newline at end of file diff --git a/modules/auth/auth_form.go b/modules/auth/auth_form.go index 5dae55c1ef9cf..4b782b3cf44cd 100644 --- a/modules/auth/auth_form.go +++ b/modules/auth/auth_form.go @@ -11,34 +11,35 @@ import ( // AuthenticationForm form for authentication type AuthenticationForm struct { - ID int64 - Type int `binding:"Range(2,6)"` - Name string `binding:"Required;MaxSize(30)"` - Host string - Port int - BindDN string - BindPassword string - UserBase string - UserDN string - AttributeUsername string - AttributeName string - AttributeSurname string - AttributeMail string - AttributesInBind bool - Filter string - AdminFilter string - IsActive bool - SMTPAuth string - SMTPHost string - SMTPPort int - AllowedDomains string - SecurityProtocol int `binding:"Range(0,2)"` - TLS bool - SkipVerify bool - PAMServiceName string - Oauth2Provider string - Oauth2Key string - Oauth2Secret string + ID int64 + Type int `binding:"Range(2,6)"` + Name string `binding:"Required;MaxSize(30)"` + Host string + Port int + BindDN string + BindPassword string + UserBase string + UserDN string + AttributeUsername string + AttributeName string + AttributeSurname string + AttributeMail string + AttributesInBind bool + Filter string + AdminFilter string + IsActive bool + SMTPAuth string + SMTPHost string + SMTPPort int + AllowedDomains string + SecurityProtocol int `binding:"Range(0,2)"` + TLS bool + SkipVerify bool + PAMServiceName string + Oauth2Provider string + Oauth2Key string + Oauth2Secret string + OpenIDConnectAutoDiscoveryURL string } // Validate validates fields diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index e529ce6bc5fc5..5d69b09d37f90 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -21,6 +21,8 @@ import ( "github.com/markbates/goth/providers/twitter" "github.com/markbates/goth/providers/facebook" "github.com/markbates/goth/providers/dropbox" + "github.com/markbates/goth/providers/openidConnect" + "math" ) var ( @@ -35,7 +37,15 @@ func Init() { log.Fatal(4, "Fail to create dir %s: %v", sessionDir, err) } - gothic.Store = sessions.NewFilesystemStore(sessionDir, []byte(sessionUsersStoreKey)) + store := sessions.NewFilesystemStore(sessionDir, []byte(sessionUsersStoreKey)) + // according to the Goth lib: + // set the maxLength of the cookies stored on the disk to a larger number to prevent issues with: + // securecookie: the value is too long + // when using OpenID Connect , since this can contain a large amount of extra information in the id_token + + // Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk + store.MaxLength(math.MaxInt64) + gothic.Store = store gothic.SetState = func(req *http.Request) string { return uuid.NewV4().String() @@ -78,12 +88,14 @@ func ProviderCallback(provider string, request *http.Request, response http.Resp } // RegisterProvider register a OAuth2 provider in goth lib -func RegisterProvider(providerName, providerType, clientID, clientSecret string) { - provider := createProvider(providerName, providerType, clientID, clientSecret) +func RegisterProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL string) error { + provider, err := createProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL) - if provider != nil { + if err == nil && provider != nil { goth.UseProviders(provider) } + + return err } // RemoveProvider removes the given OAuth2 provider from the goth lib @@ -92,10 +104,11 @@ func RemoveProvider(providerName string) { } // used to create different types of goth providers -func createProvider(providerName, providerType, clientID, clientSecret string) goth.Provider { +func createProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL string) (goth.Provider, error) { callbackURL := setting.AppURL + "user/oauth2/" + providerName + "/callback" var provider goth.Provider + var err error switch providerType { case "bitbucket": @@ -110,14 +123,18 @@ func createProvider(providerName, providerType, clientID, clientSecret string) g provider = gitlab.New(clientID, clientSecret, callbackURL) case "gplus": provider = gplus.New(clientID, clientSecret, callbackURL, "email") + case "openidConnect": + if provider, err = openidConnect.New(clientID, clientSecret, callbackURL, openIDConnectAutoDiscoveryURL); err != nil { + log.Warn("Failed to create OpenID Connect Provider with name '%s' with url '%s': %v", providerName, openIDConnectAutoDiscoveryURL, err) + } case "twitter": provider = twitter.NewAuthenticate(clientID, clientSecret, callbackURL) } // always set the name if provider is created so we can support multiple setups of 1 provider - if provider != nil { + if err == nil && provider != nil { provider.SetName(providerName) } - return provider + return provider, err } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 0d62697f38286..23434eca004c5 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1117,19 +1117,22 @@ auths.allowed_domains_helper = Leave it empty to not restrict any domains. Multi auths.enable_tls = Enable TLS Encryption auths.skip_tls_verify = Skip TLS Verify auths.pam_service_name = PAM Service Name -auths.oauth2_provider = OAuth2 provider +auths.oauth2_provider = OAuth2 Provider auths.oauth2_clientID = Client ID (Key) auths.oauth2_clientSecret = Client Secret +auths.openIdConnectAutoDiscoveryURL = OpenID Connect Auto Discovery URL auths.enable_auto_register = Enable Auto Registration auths.tips = Tips -auths.tips.oauth2.general = OAuth2 General -auths.tips.oauth2.general.tip = When registering a new OAuth2 application, the callback/redirect URL should be: /user/oauth2//callback +auths.tips.oauth2.general = OAuth2 Authentication +auths.tips.oauth2.general.tip = When registering a new OAuth2 authentication, the callback/redirect URL should be: /user/oauth2//callback +auths.tip.oauth2_provider = OAuth2 Provider auths.tip.bitbucket = Register a new OAuth consumer on https://bitbucket.org/account/user//oauth-consumers/new and add the permission "Account"-"Read" auths.tip.dropbox = Create a new App on https://www.dropbox.com/developers/apps auths.tip.facebook = Register a new App on https://developers.facebook.com/apps and add the product "Facebook Login" auths.tip.github = Register a new OAuth application on https://github.com/settings/applications/new auths.tip.gitlab = Register a new application on https://gitlab.com/profile/applications auths.tip.google_plus = Obtain OAuth2 client credentials from the Google API console (https://console.developers.google.com/) +auths.tip.openid_connect = Use the OpenID Connect Discovery URL (/.well-known/openid-configuration) to specify the endpoints auths.tip.twitter = Go to https://dev.twitter.com/apps , create an application and ensure that the “Allow this application to be used to Sign in with Twitter” option is enabled. auths.edit = Edit Authentication Setting auths.activated = This authentication is activated diff --git a/public/css/index.css b/public/css/index.css index a35351e17716a..ce84f57390ec1 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -2995,7 +2995,7 @@ footer .ui.language .menu { float: left; } .signin .oauth2 a { - margin-right: 5px; + margin-right: 3px; } .signin .oauth2 a:last-child { margin-right: 0px; @@ -3003,4 +3003,7 @@ footer .ui.language .menu { .signin .oauth2 img { width: 32px; height: 32px; +} +.signin .oauth2 img.openidConnect { + width: auto; } \ No newline at end of file diff --git a/public/img/auth/openid_connect.png b/public/img/auth/openid_connect.png new file mode 100644 index 0000000000000000000000000000000000000000..af033cd1fe1213954ea98270b4556115998b7db7 GIT binary patch literal 12921 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 0) { $('#auth_type').change(function () { @@ -1045,21 +1056,24 @@ function initAdmin() { case '6': // OAuth2 $('.oauth2').show(); $('.oauth2 input').attr('required', 'required'); + onOAuth2Change(); break; } - if (authType == '2' || authType == '5') { onSecurityProtocolChange() } }); $('#auth_type').change(); - $('#security_protocol').change(onSecurityProtocolChange) + $('#security_protocol').change(onSecurityProtocolChange); + $('#oauth2_provider').change(onOAuth2Change); } // Edit authentication if ($('.admin.edit.authentication').length > 0) { var authType = $('#auth_type').val(); if (authType == '2' || authType == '5') { $('#security_protocol').change(onSecurityProtocolChange); + } else if (authType == '6') { + $('#oauth2_provider').change(onOAuth2Change); } } diff --git a/routers/admin/auths.go b/routers/admin/auths.go index 345494b4f6695..687252352005c 100644 --- a/routers/admin/auths.go +++ b/routers/admin/auths.go @@ -124,9 +124,10 @@ func parseSMTPConfig(form auth.AuthenticationForm) *models.SMTPConfig { func parseOAuth2Config(form auth.AuthenticationForm) *models.OAuth2Config { return &models.OAuth2Config{ - Provider: form.Oauth2Provider, - ClientID: form.Oauth2Key, - ClientSecret: form.Oauth2Secret, + Provider: form.Oauth2Provider, + ClientID: form.Oauth2Key, + ClientSecret: form.Oauth2Secret, + OpenIDConnectAutoDiscoveryURL: form.OpenIDConnectAutoDiscoveryURL, } } @@ -257,7 +258,12 @@ func EditAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) { source.IsActived = form.IsActive source.Cfg = config if err := models.UpdateSource(source); err != nil { - ctx.Handle(500, "UpdateSource", err) + if models.IsErrOpenIDConnectInitialize(err) { + ctx.Flash.Error(err.Error(), true) + ctx.HTML(200, tplAuthEdit) + } else { + ctx.Handle(500, "UpdateSource", err) + } return } log.Trace("Authentication changed by admin(%s): %d", ctx.User.Name, source.ID) diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index 84b62f6e87b49..22392d8e6165d 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -166,6 +166,10 @@ +
+ + +
{{end}}
diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl index 123f46601a4dc..b8b23f2a98a39 100644 --- a/templates/admin/auth/new.tmpl +++ b/templates/admin/auth/new.tmpl @@ -156,6 +156,10 @@
+
+ + +
@@ -195,22 +199,29 @@
GMail Settings:

Host: smtp.gmail.com, Port: 587, Enable TLS Encryption: true

+
{{.i18n.Tr "admin.auths.tips.oauth2.general"}}:

{{.i18n.Tr "admin.auths.tips.oauth2.general.tip"}}

-
OAuth2 Bitbucket:
-

{{.i18n.Tr "admin.auths.tip.bitbucket"}}

-
OAuth2 Dropbox:
-

{{.i18n.Tr "admin.auths.tip.dropbox"}}

-
OAuth2 Facebook:
-

{{.i18n.Tr "admin.auths.tip.facebook"}}

-
OAuth GitHub:
-

{{.i18n.Tr "admin.auths.tip.github"}}

-
OAuth2 GitLab:
-

{{.i18n.Tr "admin.auths.tip.gitlab"}}

-
OAuth2 Google+:
-

{{.i18n.Tr "admin.auths.tip.google_plus"}}

-
OAuth2 Twitter:
-

{{.i18n.Tr "admin.auths.tip.twitter"}}

+ +
{{.i18n.Tr "admin.auths.tip.oauth2_provider"}}
+
+
  • Bitbucket
  • + {{.i18n.Tr "admin.auths.tip.bitbucket"}} +
  • Dropbox
  • + {{.i18n.Tr "admin.auths.tip.dropbox"}} +
  • Facebook
  • + {{.i18n.Tr "admin.auths.tip.facebook"}} +
  • GitHub
  • + {{.i18n.Tr "admin.auths.tip.github"}} +
  • GitLab
  • + {{.i18n.Tr "admin.auths.tip.gitlab"}} +
  • Google+
  • + {{.i18n.Tr "admin.auths.tip.google_plus"}} +
  • OpenID Connect
  • + {{.i18n.Tr "admin.auths.tip.openid_connect"}} +
  • Twitter
  • + {{.i18n.Tr "admin.auths.tip.twitter"}} +
    diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index 300764d138a7f..3c55506e4e16d 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -47,7 +47,7 @@

    {{.i18n.Tr "sign_in_with"}}

    {{range $key := .OrderedOAuth2Names}} {{$provider := index $.OAuth2Providers $key}} - {{$provider.DisplayName}} + {{$provider.DisplayName}}{{if eq $provider.Name {{end}}
    diff --git a/vendor/github.com/markbates/goth/providers/openidConnect/openidConnect.go b/vendor/github.com/markbates/goth/providers/openidConnect/openidConnect.go new file mode 100644 index 0000000000000..7ffd11c6079d4 --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/openidConnect/openidConnect.go @@ -0,0 +1,384 @@ +package openidConnect + +import ( + "net/http" + "strings" + "fmt" + "encoding/json" + "encoding/base64" + "io/ioutil" + "errors" + "golang.org/x/oauth2" + "github.com/markbates/goth" + "time" + "bytes" +) + +const ( + // Standard Claims http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + // fixed, cannot be changed + subjectClaim = "sub" + expiryClaim = "exp" + audienceClaim = "aud" + issuerClaim = "iss" + + PreferredUsernameClaim = "preferred_username" + EmailClaim = "email" + NameClaim = "name" + NicknameClaim = "nickname" + PictureClaim = "picture" + GivenNameClaim = "given_name" + FamilyNameClaim = "family_name" + AddressClaim = "address" + + // Unused but available to set in Provider claims + MiddleNameClaim = "middle_name" + ProfileClaim = "profile" + WebsiteClaim = "website" + EmailVerifiedClaim = "email_verified" + GenderClaim = "gender" + BirthdateClaim = "birthdate" + ZoneinfoClaim = "zoneinfo" + LocaleClaim = "locale" + PhoneNumberClaim = "phone_number" + PhoneNumberVerifiedClaim = "phone_number_verified" + UpdatedAtClaim = "updated_at" + + clockSkew = 10 * time.Second +) + +// Provider is the implementation of `goth.Provider` for accessing OpenID Connect provider +type Provider struct { + ClientKey string + Secret string + CallbackURL string + HTTPClient *http.Client + config *oauth2.Config + openIDConfig *OpenIDConfig + providerName string + + UserIdClaims []string + NameClaims []string + NickNameClaims []string + EmailClaims []string + AvatarURLClaims []string + FirstNameClaims []string + LastNameClaims []string + LocationClaims []string + + SkipUserInfoRequest bool +} + +type OpenIDConfig struct { + AuthEndpoint string `json:"authorization_endpoint"` + TokenEndpoint string `json:"token_endpoint"` + UserInfoEndpoint string `json:"userinfo_endpoint"` + Issuer string `json:"issuer"` +} + +// New creates a new OpenID Connect provider, and sets up important connection details. +// You should always call `openidConnect.New` to get a new Provider. Never try to create +// one manually. +// New returns an implementation of an OpenID Connect Authorization Code Flow +// See http://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth +// ID Token decryption is not (yet) supported +// UserInfo decryption is not (yet) supported +func New(clientKey, secret, callbackURL, openIDAutoDiscoveryURL string, scopes ...string) (*Provider, error) { + p := &Provider{ + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + + UserIdClaims: []string{subjectClaim}, + NameClaims: []string{NameClaim}, + NickNameClaims: []string{NicknameClaim, PreferredUsernameClaim}, + EmailClaims: []string{EmailClaim}, + AvatarURLClaims:[]string{PictureClaim}, + FirstNameClaims:[]string{GivenNameClaim}, + LastNameClaims: []string{FamilyNameClaim}, + LocationClaims: []string{AddressClaim}, + + providerName: "openid-connect", + } + + openIDConfig, err := getOpenIDConfig(p, openIDAutoDiscoveryURL) + if err != nil { + return nil, err + } + p.openIDConfig = openIDConfig + + p.config = newConfig(p, scopes, openIDConfig) + return p, nil +} + +// Name is the name used to retrieve this provider later. +func (p *Provider) Name() string { + return p.providerName +} + +// SetName is to update the name of the provider (needed in case of multiple providers of 1 type) +func (p *Provider) SetName(name string) { + p.providerName = name +} + +func (p *Provider) Client() *http.Client { + return goth.HTTPClientWithFallBack(p.HTTPClient) +} + +// Debug is a no-op for the openidConnect package. +func (p *Provider) Debug(debug bool) {} + +// BeginAuth asks the OpenID Connect provider for an authentication end-point. +func (p *Provider) BeginAuth(state string) (goth.Session, error) { + url := p.config.AuthCodeURL(state) + session := &Session{ + AuthURL: url, + } + return session, nil +} + +// FetchUser will use the the id_token and access requested information about the user. +func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { + sess := session.(*Session) + + expiresAt := sess.ExpiresAt + + if sess.IDToken == "" { + return goth.User{}, fmt.Errorf("%s cannot get user information without id_token", p.providerName) + } + + // decode returned id token to get expiry + claims, err := decodeJWT(sess.IDToken) + + if err != nil { + return goth.User{}, fmt.Errorf("oauth2: error decoding JWT token: %v", err) + } + + expiry, err := p.validateClaims(claims) + if err != nil { + return goth.User{}, fmt.Errorf("oauth2: error validating JWT token: %v", err) + } + + if expiry.Before(expiresAt) { + expiresAt = expiry + } + + if err := p.getUserInfo(sess.AccessToken, claims); err != nil { + return goth.User{}, err + } + + user := goth.User{ + AccessToken: sess.AccessToken, + Provider: p.Name(), + RefreshToken: sess.RefreshToken, + ExpiresAt: expiresAt, + RawData: claims, + } + + p.userFromClaims(claims, &user) + return user, err +} + +//RefreshTokenAvailable refresh token is provided by auth provider or not +func (p *Provider) RefreshTokenAvailable() bool { + return true +} + +//RefreshToken get new access token based on the refresh token +func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + token := &oauth2.Token{RefreshToken: refreshToken} + ts := p.config.TokenSource(oauth2.NoContext, token) + newToken, err := ts.Token() + if err != nil { + return nil, err + } + return newToken, err +} + +// validate according to standard, returns expiry +// http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation +func (p *Provider) validateClaims(claims map[string]interface{}) (time.Time, error) { + audience := getClaimValue(claims, []string{audienceClaim}) + if audience != p.ClientKey { + return time.Time{}, errors.New("audience in token does not match client key") + } + + issuer := getClaimValue(claims, []string{issuerClaim}) + if issuer != p.openIDConfig.Issuer { + return time.Time{}, errors.New("issuer in token does not match issuer in OpenIDConfig discovery") + } + + // expiry is required for JWT, not for UserInfoResponse + // is actually a int64, so force it in to that type + expiryClaim := int64(claims[expiryClaim].(float64)) + expiry := time.Unix(expiryClaim, 0) + if expiry.Add(clockSkew).Before(time.Now()) { + return time.Time{}, errors.New("user info JWT token is expired") + } + return expiry, nil +} + +func (p *Provider) userFromClaims(claims map[string]interface{}, user *goth.User) { + // required + user.UserID = getClaimValue(claims, p.UserIdClaims) + + user.Name = getClaimValue(claims, p.NameClaims) + user.NickName = getClaimValue(claims, p.NickNameClaims) + user.Email = getClaimValue(claims, p.EmailClaims) + user.AvatarURL = getClaimValue(claims, p.AvatarURLClaims) + user.FirstName = getClaimValue(claims, p.FirstNameClaims) + user.LastName = getClaimValue(claims, p.LastNameClaims) + user.Location = getClaimValue(claims, p.LocationClaims) +} + +func (p *Provider) getUserInfo(accessToken string, claims map[string]interface{}) error { + // skip if there is no UserInfoEndpoint or is explicitly disabled + if p.openIDConfig.UserInfoEndpoint == "" || p.SkipUserInfoRequest { + return nil + } + + userInfoClaims, err := p.fetchUserInfo(p.openIDConfig.UserInfoEndpoint, accessToken) + if err != nil { + return err + } + + // The sub (subject) Claim MUST always be returned in the UserInfo Response. + // http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse + userInfoSubject := getClaimValue(userInfoClaims, []string{subjectClaim}) + if userInfoSubject == "" { + return fmt.Errorf("userinfo response did not contain a 'sub' claim: %#v", userInfoClaims) + } + + // The sub Claim in the UserInfo Response MUST be verified to exactly match the sub Claim in the ID Token; + // if they do not match, the UserInfo Response values MUST NOT be used. + // http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse + subject := getClaimValue(claims, []string{subjectClaim}) + if userInfoSubject != subject { + return fmt.Errorf("userinfo 'sub' claim (%s) did not match id_token 'sub' claim (%s)", userInfoSubject, subject) + } + + // Merge in userinfo claims in case id_token claims contained some that userinfo did not + for k, v := range userInfoClaims { + claims[k] = v + } + + return nil +} + +// fetch and decode JSON from the given UserInfo URL +func (p *Provider) fetchUserInfo(url, accessToken string) (map[string]interface{}, error) { + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken)) + + resp, err := p.Client().Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Non-200 response from UserInfo: %d, WWW-Authenticate=%s", resp.StatusCode, resp.Header.Get("WWW-Authenticate")) + } + + // The UserInfo Claims MUST be returned as the members of a JSON object + // http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return unMarshal(data) +} + +func getOpenIDConfig(p *Provider, openIDAutoDiscoveryURL string) (*OpenIDConfig, error) { + res, err := p.Client().Get(openIDAutoDiscoveryURL) + if err != nil { + return nil, err + } + defer res.Body.Close() + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + + openIDConfig := &OpenIDConfig{} + err = json.Unmarshal(body, openIDConfig) + if err != nil { + return nil, err + } + + return openIDConfig, nil +} + +func newConfig(provider *Provider, scopes []string, openIDConfig *OpenIDConfig) *oauth2.Config { + c := &oauth2.Config{ + ClientID: provider.ClientKey, + ClientSecret: provider.Secret, + RedirectURL: provider.CallbackURL, + Endpoint: oauth2.Endpoint{ + AuthURL: openIDConfig.AuthEndpoint, + TokenURL: openIDConfig.TokenEndpoint, + }, + Scopes: []string{}, + } + + if len(scopes) > 0 { + foundOpenIDScope := false + + for _, scope := range scopes { + if scope == "openid" { + foundOpenIDScope = true + } + c.Scopes = append(c.Scopes, scope) + } + + if !foundOpenIDScope { + c.Scopes = append(c.Scopes, "openid") + } + } else { + c.Scopes = []string{"openid"} + } + + return c +} + +func getClaimValue(data map[string]interface{}, claims []string) string { + for _, claim := range claims { + if value, ok := data[claim]; ok { + if stringValue, ok := value.(string); ok && len(stringValue) > 0 { + return stringValue + } + } + } + + return "" +} + +// decodeJWT decodes a JSON Web Token into a simple map +// http://openid.net/specs/draft-jones-json-web-token-07.html +func decodeJWT(jwt string) (map[string]interface{}, error) { + jwtParts := strings.Split(jwt, ".") + if len(jwtParts) != 3 { + return nil, errors.New("jws: invalid token received, not all parts available") + } + + // Re-pad, if needed + encodedPayload := jwtParts[1] + if l := len(encodedPayload) % 4; l != 0 { + encodedPayload += strings.Repeat("=", 4-l) + } + + decodedPayload, err := base64.StdEncoding.DecodeString(encodedPayload) + if err != nil { + return nil, err + } + + return unMarshal(decodedPayload) +} + +func unMarshal(payload []byte) (map[string]interface{}, error) { + data := make(map[string]interface{}) + + return data, json.NewDecoder(bytes.NewBuffer(payload)).Decode(&data) +} diff --git a/vendor/github.com/markbates/goth/providers/openidConnect/session.go b/vendor/github.com/markbates/goth/providers/openidConnect/session.go new file mode 100644 index 0000000000000..a34584fdef15b --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/openidConnect/session.go @@ -0,0 +1,63 @@ +package openidConnect + +import ( + "errors" + "github.com/markbates/goth" + "encoding/json" + "strings" + "time" + "golang.org/x/oauth2" +) + +// Session stores data during the auth process with the OpenID Connect provider. +type Session struct { + AuthURL string + AccessToken string + RefreshToken string + ExpiresAt time.Time + IDToken string +} + +// GetAuthURL will return the URL set by calling the `BeginAuth` function on the OpenID Connect provider. +func (s Session) GetAuthURL() (string, error) { + if s.AuthURL == "" { + return "", errors.New("an AuthURL has not be set") + } + return s.AuthURL, nil +} + +// Authorize the session with the OpenID Connect provider and return the access token to be stored for future use. +func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { + p := provider.(*Provider) + token, err := p.config.Exchange(oauth2.NoContext, params.Get("code")) + if err != nil { + return "", err + } + + if !token.Valid() { + return "", errors.New("Invalid token received from provider") + } + + s.AccessToken = token.AccessToken + s.RefreshToken = token.RefreshToken + s.ExpiresAt = token.Expiry + s.IDToken = token.Extra("id_token").(string) + return token.AccessToken, err +} + +// Marshal the session into a string +func (s Session) Marshal() string { + b, _ := json.Marshal(s) + return string(b) +} + +func (s Session) String() string { + return s.Marshal() +} + +// UnmarshalSession will unmarshal a JSON string into a session. +func (p *Provider) UnmarshalSession(data string) (goth.Session, error) { + sess := &Session{} + err := json.NewDecoder(strings.NewReader(data)).Decode(sess) + return sess, err +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 884a5fe33bde0..dcd583880a6d7 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -598,6 +598,12 @@ "revision": "a0a751ca505adde67bc2c92e2aae99531b1e3213", "revisionTime": "2017-02-20T13:56:36Z" }, + { + "checksumSHA1": "sMYKhqAUZXM1+T/TjlMhWh8Vveo=", + "path": "github.com/markbates/goth/providers/openidConnect", + "revision": "7276be0fdf719ddff753f3574ef0f967e4a5a5f7", + "revisionTime": "2017-02-20T14:02:47Z" + }, { "checksumSHA1": "1w0V6jYXaGlEtZcMeYTOAAucvgw=", "path": "github.com/markbates/goth/providers/twitter", From 94c8a48a226a82b4d48c2f1310eb88c02e852f22 Mon Sep 17 00:00:00 2001 From: willemvd Date: Wed, 22 Feb 2017 14:10:25 +0100 Subject: [PATCH 10/19] lower the amount of disk storage for each session to prevent issues while building cross platform (and disk overflow) --- modules/auth/oauth2/oauth2.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index 5d69b09d37f90..b9f45e5163a84 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -44,7 +44,7 @@ func Init() { // when using OpenID Connect , since this can contain a large amount of extra information in the id_token // Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk - store.MaxLength(math.MaxInt64) + store.MaxLength(math.MaxInt16) gothic.Store = store gothic.SetState = func(req *http.Request) string { From 8b8b5f4708680cd3fa61ef872b24cee158d72c0a Mon Sep 17 00:00:00 2001 From: willemvd Date: Wed, 22 Feb 2017 15:36:33 +0100 Subject: [PATCH 11/19] imports according to goimport and code style --- models/login_source.go | 16 ++++++++-------- modules/auth/oauth2/oauth2.go | 23 ++++++++++++----------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/models/login_source.go b/models/login_source.go index 4e76108574af9..06d64ccf6a728 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -11,19 +11,18 @@ import ( "fmt" "net/smtp" "net/textproto" + "sort" "strings" "time" + "code.gitea.io/gitea/modules/auth/ldap" + "code.gitea.io/gitea/modules/auth/oauth2" + "code.gitea.io/gitea/modules/auth/pam" + "code.gitea.io/gitea/modules/log" "github.com/Unknwon/com" "github.com/go-macaron/binding" "github.com/go-xorm/core" "github.com/go-xorm/xorm" - - "code.gitea.io/gitea/modules/auth/ldap" - "code.gitea.io/gitea/modules/auth/pam" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/auth/oauth2" - "sort" ) // LoginType represents an login type. @@ -763,11 +762,12 @@ func InitOAuth2() { oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL) } } + // wrapOpenIDConnectInitializeError is used to wrap the error but this cannot be done in modules/auth/oauth2 // inside oauth2: import cycle not allowed models -> modules/auth/oauth2 -> models func wrapOpenIDConnectInitializeError(err error, providerName string, oAuth2Config *OAuth2Config) error { if err != nil && "openidConnect" == oAuth2Config.Provider { - err = ErrOpenIDConnectInitialize{ProviderName:providerName,OpenIDConnectAutoDiscoveryURL:oAuth2Config.OpenIDConnectAutoDiscoveryURL,Cause:err} + err = ErrOpenIDConnectInitialize{ProviderName: providerName, OpenIDConnectAutoDiscoveryURL: oAuth2Config.OpenIDConnectAutoDiscoveryURL, Cause: err} } return err -} \ No newline at end of file +} diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index b9f45e5163a84..b53aed6c2d187 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -5,24 +5,25 @@ package oauth2 import ( - "code.gitea.io/gitea/modules/setting" + "math" + "net/http" + "os" + "path/filepath" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "github.com/gorilla/sessions" "github.com/markbates/goth" "github.com/markbates/goth/gothic" - "net/http" - "os" - "github.com/satori/go.uuid" - "path/filepath" - "github.com/markbates/goth/providers/github" - "github.com/markbates/goth/providers/gplus" - "github.com/markbates/goth/providers/gitlab" "github.com/markbates/goth/providers/bitbucket" - "github.com/markbates/goth/providers/twitter" - "github.com/markbates/goth/providers/facebook" "github.com/markbates/goth/providers/dropbox" + "github.com/markbates/goth/providers/facebook" + "github.com/markbates/goth/providers/github" + "github.com/markbates/goth/providers/gitlab" + "github.com/markbates/goth/providers/gplus" "github.com/markbates/goth/providers/openidConnect" - "math" + "github.com/markbates/goth/providers/twitter" + "github.com/satori/go.uuid" ) var ( From e2544aec47dfcf98523071034bb659c3d2fadf21 Mon Sep 17 00:00:00 2001 From: willemvd Date: Fri, 24 Feb 2017 16:17:01 +0100 Subject: [PATCH 12/19] make it possible to set custom urls to gitlab and github provider (only these could have a different host) --- models/login_source.go | 45 ++++++--- modules/auth/auth_form.go | 5 + modules/auth/oauth2/oauth2.go | 92 ++++++++++++++++++- options/locale/locale_en-US.ini | 5 + public/js/index.js | 56 +++++++++-- routers/admin/auths.go | 22 ++++- templates/admin/auth/edit.tmpl | 31 ++++++- templates/admin/auth/new.tmpl | 31 ++++++- .../markbates/goth/providers/github/github.go | 31 ++++--- .../markbates/goth/providers/gitlab/gitlab.go | 27 ++++-- vendor/vendor.json | 44 ++++----- 11 files changed, 319 insertions(+), 70 deletions(-) diff --git a/models/login_source.go b/models/login_source.go index 06d64ccf6a728..7b422ed57be87 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -125,6 +125,7 @@ type OAuth2Config struct { ClientID string ClientSecret string OpenIDConnectAutoDiscoveryURL string + CustomURLMapping *oauth2.CustomURLMapping } // FromDB fills up an OAuth2Config from serialized format. @@ -297,7 +298,7 @@ func CreateLoginSource(source *LoginSource) error { _, err = x.Insert(source) if err == nil && source.IsOAuth2() && source.IsActived { oAuth2Config := source.OAuth2() - err = oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL) + err = oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL, oAuth2Config.CustomURLMapping) err = wrapOpenIDConnectInitializeError(err, source.Name, oAuth2Config) if err != nil { @@ -340,7 +341,7 @@ func UpdateSource(source *LoginSource) error { _, err := x.Id(source.ID).AllCols().Update(source) if err == nil && source.IsOAuth2() && source.IsActived { oAuth2Config := source.OAuth2() - err = oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL) + err = oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL, oAuth2Config.CustomURLMapping) err = wrapOpenIDConnectInitializeError(err, source.Name, oAuth2Config) if err != nil { @@ -610,25 +611,47 @@ func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMCon // OAuth2Provider describes the display values of a single OAuth2 provider type OAuth2Provider struct { - Name string - DisplayName string - Image string + Name string + DisplayName string + Image string + CustomURLMapping *oauth2.CustomURLMapping } // OAuth2Providers contains the map of registered OAuth2 providers in Gitea (based on goth) // key is used to map the OAuth2Provider with the goth provider type (also in LoginSource.OAuth2Config.Provider) // value is used to store display data var OAuth2Providers = map[string]OAuth2Provider{ - "bitbucket": {Name: "bitbucket", DisplayName: "Bitbucket", Image: "/img/auth/bitbucket.png"}, - "dropbox": {Name: "dropbox", DisplayName: "Dropbox", Image: "/img/auth/dropbox.png"}, - "facebook": {Name: "facebook", DisplayName: "Facebook", Image: "/img/auth/facebook.png"}, - "github": {Name: "github", DisplayName: "GitHub", Image: "/img/auth/github.png"}, - "gitlab": {Name: "gitlab", DisplayName: "GitLab", Image: "/img/auth/gitlab.png"}, + "bitbucket": {Name: "bitbucket", DisplayName: "Bitbucket", Image: "/img/auth/bitbucket.png"}, + "dropbox": {Name: "dropbox", DisplayName: "Dropbox", Image: "/img/auth/dropbox.png"}, + "facebook": {Name: "facebook", DisplayName: "Facebook", Image: "/img/auth/facebook.png"}, + "github": {Name: "github", DisplayName: "GitHub", Image: "/img/auth/github.png", + CustomURLMapping: &oauth2.CustomURLMapping{ + TokenURL: oauth2.GetDefaultTokenURL("github"), + AuthURL: oauth2.GetDefaultAuthURL("github"), + ProfileURL: oauth2.GetDefaultProfileURL("github"), + EmailURL: oauth2.GetDefaultEmailURL("github"), + }, + }, + "gitlab": {Name: "gitlab", DisplayName: "GitLab", Image: "/img/auth/gitlab.png", + CustomURLMapping: &oauth2.CustomURLMapping{ + TokenURL: oauth2.GetDefaultTokenURL("gitlab"), + AuthURL: oauth2.GetDefaultAuthURL("gitlab"), + ProfileURL: oauth2.GetDefaultProfileURL("gitlab"), + }, + }, "gplus": {Name: "gplus", DisplayName: "Google+", Image: "/img/auth/google_plus.png"}, "openidConnect": {Name: "openidConnect", DisplayName: "OpenID Connect", Image: "/img/auth/openid_connect.png"}, "twitter": {Name: "twitter", DisplayName: "Twitter", Image: "/img/auth/twitter.png"}, } +// OAuth2DefaultCustomURLMappings contains the map of default URL's for OAuth2 providers that are allowed to have custom urls +// key is used to map the OAuth2Provider +// value is the mapping as defined for the OAuth2Provider +var OAuth2DefaultCustomURLMappings = map[string]*oauth2.CustomURLMapping { + "github": OAuth2Providers["github"].CustomURLMapping, + "gitlab": OAuth2Providers["gitlab"].CustomURLMapping, +} + // ExternalUserLogin attempts a login using external source types. func ExternalUserLogin(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) { if !source.IsActived { @@ -759,7 +782,7 @@ func InitOAuth2() { for _, source := range loginSources { oAuth2Config := source.OAuth2() - oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL) + oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL, oAuth2Config.CustomURLMapping) } } diff --git a/modules/auth/auth_form.go b/modules/auth/auth_form.go index 4b782b3cf44cd..8dc039835fdf7 100644 --- a/modules/auth/auth_form.go +++ b/modules/auth/auth_form.go @@ -40,6 +40,11 @@ type AuthenticationForm struct { Oauth2Key string Oauth2Secret string OpenIDConnectAutoDiscoveryURL string + Oauth2UseCustomURL bool + Oauth2TokenURL string + Oauth2AuthURL string + Oauth2ProfileURL string + Oauth2EmailURL string } // Validate validates fields diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index b53aed6c2d187..3c2cd681263e4 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -31,6 +31,14 @@ var ( providerHeaderKey = "gitea-oauth2-provider" ) +// CustomURLMapping describes the urls values to use when customizing OAuth2 provider URLs +type CustomURLMapping struct { + AuthURL string + TokenURL string + ProfileURL string + EmailURL string +} + // Init initialize the setup of the OAuth2 library func Init() { sessionDir := filepath.Join(setting.AppDataPath, "sessions", "oauth2") @@ -89,8 +97,8 @@ func ProviderCallback(provider string, request *http.Request, response http.Resp } // RegisterProvider register a OAuth2 provider in goth lib -func RegisterProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL string) error { - provider, err := createProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL) +func RegisterProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL string, customURLMapping *CustomURLMapping) error { + provider, err := createProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL, customURLMapping) if err == nil && provider != nil { goth.UseProviders(provider) @@ -105,7 +113,7 @@ func RemoveProvider(providerName string) { } // used to create different types of goth providers -func createProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL string) (goth.Provider, error) { +func createProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL string, customURLMapping *CustomURLMapping) (goth.Provider, error) { callbackURL := setting.AppURL + "user/oauth2/" + providerName + "/callback" var provider goth.Provider @@ -119,9 +127,41 @@ func createProvider(providerName, providerType, clientID, clientSecret, openIDCo case "facebook": provider = facebook.New(clientID, clientSecret, callbackURL, "email") case "github": - provider = github.New(clientID, clientSecret, callbackURL, "user:email") + authURL := github.AuthURL + tokenURL := github.TokenURL + profileURL := github.ProfileURL + emailURL := github.EmailURL + if customURLMapping != nil { + if len(customURLMapping.AuthURL) > 0 { + authURL = customURLMapping.AuthURL + } + if len(customURLMapping.TokenURL) > 0 { + tokenURL = customURLMapping.TokenURL + } + if len(customURLMapping.ProfileURL) > 0 { + profileURL = customURLMapping.ProfileURL + } + if len(customURLMapping.EmailURL) > 0 { + emailURL = customURLMapping.EmailURL + } + } + provider = github.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL, emailURL) case "gitlab": - provider = gitlab.New(clientID, clientSecret, callbackURL) + authURL := gitlab.AuthURL + tokenURL := gitlab.TokenURL + profileURL := gitlab.ProfileURL + if customURLMapping != nil { + if len(customURLMapping.AuthURL) > 0 { + authURL = customURLMapping.AuthURL + } + if len(customURLMapping.TokenURL) > 0 { + tokenURL = customURLMapping.TokenURL + } + if len(customURLMapping.ProfileURL) > 0 { + profileURL = customURLMapping.ProfileURL + } + } + provider = gitlab.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL) case "gplus": provider = gplus.New(clientID, clientSecret, callbackURL, "email") case "openidConnect": @@ -139,3 +179,45 @@ func createProvider(providerName, providerType, clientID, clientSecret, openIDCo return provider, err } + +// GetDefaultTokenURL return the default token url for the given provider +func GetDefaultTokenURL(provider string) string { + switch provider { + case "github": + return github.TokenURL + case "gitlab": + return gitlab.TokenURL + } + return "" +} + +// GetDefaultAuthURL return the default authorize url for the given provider +func GetDefaultAuthURL(provider string) string { + switch provider { + case "github": + return github.AuthURL + case "gitlab": + return gitlab.AuthURL + } + return "" +} + +// GetDefaultProfileURL return the default profile url for the given provider +func GetDefaultProfileURL(provider string) string { + switch provider { + case "github": + return github.ProfileURL + case "gitlab": + return gitlab.ProfileURL + } + return "" +} + +// GetDefaultEmailURL return the default email url for the given provider +func GetDefaultEmailURL(provider string) string { + switch provider { + case "github": + return github.EmailURL + } + return "" +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 23434eca004c5..40a380787e373 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1121,6 +1121,11 @@ auths.oauth2_provider = OAuth2 Provider auths.oauth2_clientID = Client ID (Key) auths.oauth2_clientSecret = Client Secret auths.openIdConnectAutoDiscoveryURL = OpenID Connect Auto Discovery URL +auths.oauth2_use_custom_url = Use custom URLs instead of default URLs +auths.oauth2_tokenURL = Token URL +auths.oauth2_authURL = Authorize URL +auths.oauth2_profileURL = Profile URL +auths.oauth2_emailURL = Email URL auths.enable_auto_register = Enable Auto Registration auths.tips = Tips auths.tips.oauth2.general = OAuth2 Authentication diff --git a/public/js/index.js b/public/js/index.js index 5375a8aaceba1..7239a1578fdf7 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -1017,13 +1017,52 @@ function initAdmin() { } function onOAuth2Change() { + $('.open_id_connect_auto_discovery_url, .oauth2_use_custom_url').hide(); + $('.open_id_connect_auto_discovery_url input[required]').removeAttr('required'); + var provider = $('#oauth2_provider').val(); - if (provider == 'openidConnect') { - $('#open_id_connect_auto_discovery_url input').attr('required', 'required'); - $('.openid-connect-auto-discovery-url').show(); - } else { - $('#open_id_connect_auto_discovery_url input[required]').removeAttr('required'); - $('.openid-connect-auto-discovery-url').hide(); + switch (provider) { + case 'github': + case 'gitlab': + $('.oauth2_use_custom_url').show(); + break; + case 'openidConnect': + $('.open_id_connect_auto_discovery_url input').attr('required', 'required'); + $('.open_id_connect_auto_discovery_url').show(); + break; + } + onOAuth2UseCustomURLChange(); + } + + function onOAuth2UseCustomURLChange() { + var provider = $('#oauth2_provider').val(); + $('.oauth2_use_custom_url_field').hide(); + $('.oauth2_use_custom_url_field input[required]').removeAttr('required'); + + if ($('#oauth2_use_custom_url').is(':checked')) { + if (!$('#oauth2_token_url').val()) { + $('#oauth2_token_url').val($('#' + provider + '_token_url').val()); + } + if (!$('#oauth2_auth_url').val()) { + $('#oauth2_auth_url').val($('#' + provider + '_auth_url').val()); + } + if (!$('#oauth2_profile_url').val()) { + $('#oauth2_profile_url').val($('#' + provider + '_profile_url').val()); + } + if (!$('#oauth2_email_url').val()) { + $('#oauth2_email_url').val($('#' + provider + '_email_url').val()); + } + switch (provider) { + case 'github': + $('.oauth2_token_url input, .oauth2_auth_url input, .oauth2_profile_url input, .oauth2_email_url input').attr('required', 'required'); + $('.oauth2_token_url, .oauth2_auth_url, .oauth2_profile_url, .oauth2_email_url').show(); + break; + case 'gitlab': + $('.oauth2_token_url input, .oauth2_auth_url input, .oauth2_profile_url input').attr('required', 'required'); + $('.oauth2_token_url, .oauth2_auth_url, .oauth2_profile_url').show(); + $('#oauth2_email_url').val(''); + break; + } } } @@ -1055,7 +1094,7 @@ function initAdmin() { break; case '6': // OAuth2 $('.oauth2').show(); - $('.oauth2 input').attr('required', 'required'); + $('.oauth2 div.required:not(.oauth2_use_custom_url,.oauth2_use_custom_url_field,.open_id_connect_auto_discovery_url) input').attr('required', 'required'); onOAuth2Change(); break; } @@ -1066,6 +1105,7 @@ function initAdmin() { $('#auth_type').change(); $('#security_protocol').change(onSecurityProtocolChange); $('#oauth2_provider').change(onOAuth2Change); + $('#oauth2_use_custom_url').change(onOAuth2UseCustomURLChange); } // Edit authentication if ($('.admin.edit.authentication').length > 0) { @@ -1074,6 +1114,8 @@ function initAdmin() { $('#security_protocol').change(onSecurityProtocolChange); } else if (authType == '6') { $('#oauth2_provider').change(onOAuth2Change); + $('#oauth2_use_custom_url').change(onOAuth2UseCustomURLChange); + onOAuth2Change(); } } diff --git a/routers/admin/auths.go b/routers/admin/auths.go index 687252352005c..4d3ecac12f40e 100644 --- a/routers/admin/auths.go +++ b/routers/admin/auths.go @@ -7,16 +7,16 @@ package admin import ( "fmt" - "github.com/Unknwon/com" - "github.com/go-xorm/core" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/auth/ldap" + "code.gitea.io/gitea/modules/auth/oauth2" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "github.com/Unknwon/com" + "github.com/go-xorm/core" ) const ( @@ -77,6 +77,7 @@ func NewAuthSource(ctx *context.Context) { ctx.Data["SecurityProtocols"] = securityProtocols ctx.Data["SMTPAuths"] = models.SMTPAuths ctx.Data["OAuth2Providers"] = models.OAuth2Providers + ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings // only the first as default for key := range models.OAuth2Providers { @@ -123,11 +124,23 @@ func parseSMTPConfig(form auth.AuthenticationForm) *models.SMTPConfig { } func parseOAuth2Config(form auth.AuthenticationForm) *models.OAuth2Config { + var customURLMapping *oauth2.CustomURLMapping + if form.Oauth2UseCustomURL { + customURLMapping = &oauth2.CustomURLMapping{ + TokenURL: form.Oauth2TokenURL, + AuthURL: form.Oauth2AuthURL, + ProfileURL: form.Oauth2ProfileURL, + EmailURL: form.Oauth2EmailURL, + } + } else { + customURLMapping = nil + } return &models.OAuth2Config{ Provider: form.Oauth2Provider, ClientID: form.Oauth2Key, ClientSecret: form.Oauth2Secret, OpenIDConnectAutoDiscoveryURL: form.OpenIDConnectAutoDiscoveryURL, + CustomURLMapping: customURLMapping, } } @@ -143,6 +156,7 @@ func NewAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) { ctx.Data["SecurityProtocols"] = securityProtocols ctx.Data["SMTPAuths"] = models.SMTPAuths ctx.Data["OAuth2Providers"] = models.OAuth2Providers + ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings hasTLS := false var config core.Conversion @@ -200,6 +214,7 @@ func EditAuthSource(ctx *context.Context) { ctx.Data["SecurityProtocols"] = securityProtocols ctx.Data["SMTPAuths"] = models.SMTPAuths ctx.Data["OAuth2Providers"] = models.OAuth2Providers + ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid")) if err != nil { @@ -223,6 +238,7 @@ func EditAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) { ctx.Data["SMTPAuths"] = models.SMTPAuths ctx.Data["OAuth2Providers"] = models.OAuth2Providers + ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid")) if err != nil { diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index 22392d8e6165d..b760d42d0e038 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -166,10 +166,39 @@ -
    +
    + +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + {{if .OAuth2DefaultCustomURLMappings}}{{range $key, $value := .OAuth2DefaultCustomURLMappings}} + + + + + {{end}}{{end}} {{end}}
    diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl index b8b23f2a98a39..6be12f38801a7 100644 --- a/templates/admin/auth/new.tmpl +++ b/templates/admin/auth/new.tmpl @@ -156,10 +156,39 @@
    -
    +
    + +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + {{if .OAuth2DefaultCustomURLMappings}}{{range $key, $value := .OAuth2DefaultCustomURLMappings}} + + + + + {{end}}{{end}}
    diff --git a/vendor/github.com/markbates/goth/providers/github/github.go b/vendor/github.com/markbates/goth/providers/github/github.go index 866150e63a881..b3c29b9670c00 100644 --- a/vendor/github.com/markbates/goth/providers/github/github.go +++ b/vendor/github.com/markbates/goth/providers/github/github.go @@ -37,13 +37,20 @@ var ( // You should always call `github.New` to get a new Provider. Never try to create // one manually. func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { + return NewCustomisedURL(clientKey, secret, callbackURL, AuthURL, TokenURL, ProfileURL, EmailURL, scopes...) +} + +// NewCustomisedURL is similar to New(...) but can be used to set custom URLs to connect to +func NewCustomisedURL(clientKey, secret, callbackURL, authURL, tokenURL, profileURL, emailURL string, scopes ...string) *Provider { p := &Provider{ - ClientKey: clientKey, - Secret: secret, - CallbackURL: callbackURL, - providerName: "github", - } - p.config = newConfig(p, scopes) + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + providerName: "github", + profileURL: profileURL, + emailURL: emailURL, + } + p.config = newConfig(p, authURL, tokenURL, scopes) return p } @@ -55,6 +62,8 @@ type Provider struct { HTTPClient *http.Client config *oauth2.Config providerName string + profileURL string + emailURL string } // Name is the name used to retrieve this provider later. @@ -96,7 +105,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) } - response, err := p.Client().Get(ProfileURL + "?access_token=" + url.QueryEscape(sess.AccessToken)) + response, err := p.Client().Get(p.profileURL + "?access_token=" + url.QueryEscape(sess.AccessToken)) if err != nil { return user, err } @@ -163,7 +172,7 @@ func userFromReader(reader io.Reader, user *goth.User) error { } func getPrivateMail(p *Provider, sess *Session) (email string, err error) { - response, err := p.Client().Get(EmailURL + "?access_token=" + url.QueryEscape(sess.AccessToken)) + response, err := p.Client().Get(p.emailURL + "?access_token=" + url.QueryEscape(sess.AccessToken)) if err != nil { if response != nil { response.Body.Close() @@ -194,14 +203,14 @@ func getPrivateMail(p *Provider, sess *Session) (email string, err error) { return } -func newConfig(provider *Provider, scopes []string) *oauth2.Config { +func newConfig(provider *Provider, authURL, tokenURL string, scopes []string) *oauth2.Config { c := &oauth2.Config{ ClientID: provider.ClientKey, ClientSecret: provider.Secret, RedirectURL: provider.CallbackURL, Endpoint: oauth2.Endpoint{ - AuthURL: AuthURL, - TokenURL: TokenURL, + AuthURL: authURL, + TokenURL: tokenURL, }, Scopes: []string{}, } diff --git a/vendor/github.com/markbates/goth/providers/gitlab/gitlab.go b/vendor/github.com/markbates/goth/providers/gitlab/gitlab.go index 7e704ffed6484..fe188c01a99ed 100644 --- a/vendor/github.com/markbates/goth/providers/gitlab/gitlab.go +++ b/vendor/github.com/markbates/goth/providers/gitlab/gitlab.go @@ -37,19 +37,28 @@ type Provider struct { HTTPClient *http.Client config *oauth2.Config providerName string + authURL string + tokenURL string + profileURL string } // New creates a new Gitlab provider and sets up important connection details. // You should always call `gitlab.New` to get a new provider. Never try to // create one manually. func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { + return NewCustomisedURL(clientKey, secret, callbackURL, AuthURL, TokenURL, ProfileURL, scopes...) +} + +// NewCustomisedURL is similar to New(...) but can be used to set custom URLs to connect to +func NewCustomisedURL(clientKey, secret, callbackURL, authURL, tokenURL, profileURL string, scopes ...string) *Provider { p := &Provider{ - ClientKey: clientKey, - Secret: secret, - CallbackURL: callbackURL, - providerName: "gitlab", + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + providerName: "gitlab", + profileURL: profileURL, } - p.config = newConfig(p, scopes) + p.config = newConfig(p, authURL, tokenURL, scopes) return p } @@ -92,7 +101,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) } - response, err := p.Client().Get(ProfileURL + "?access_token=" + url.QueryEscape(sess.AccessToken)) + response, err := p.Client().Get(p.profileURL + "?access_token=" + url.QueryEscape(sess.AccessToken)) if err != nil { if response != nil { response.Body.Close() @@ -121,14 +130,14 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { return user, err } -func newConfig(provider *Provider, scopes []string) *oauth2.Config { +func newConfig(provider *Provider, authURL, tokenURL string, scopes []string) *oauth2.Config { c := &oauth2.Config{ ClientID: provider.ClientKey, ClientSecret: provider.Secret, RedirectURL: provider.CallbackURL, Endpoint: oauth2.Endpoint{ - AuthURL: AuthURL, - TokenURL: TokenURL, + AuthURL: authURL, + TokenURL: tokenURL, }, Scopes: []string{}, } diff --git a/vendor/vendor.json b/vendor/vendor.json index dcd583880a6d7..b0185f9b18645 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -553,62 +553,62 @@ { "checksumSHA1": "O3KUfEXQPfdQ+tCMpP2RAIRJJqY=", "path": "github.com/markbates/goth", - "revision": "450379d2950a65070b23cc93c53436553add4484", - "revisionTime": "2017-02-06T19:46:32Z" + "revision": "90362394a367f9d77730911973462a53d69662ba", + "revisionTime": "2017-02-23T14:12:10Z" }, { "checksumSHA1": "MkFKwLV3icyUo4oP0BgEs+7+R1Y=", "path": "github.com/markbates/goth/gothic", - "revision": "450379d2950a65070b23cc93c53436553add4484", - "revisionTime": "2017-02-06T19:46:32Z" + "revision": "90362394a367f9d77730911973462a53d69662ba", + "revisionTime": "2017-02-23T14:12:10Z" }, { "checksumSHA1": "crNSlQADjX6hcxykON2tFCqY4iw=", "path": "github.com/markbates/goth/providers/bitbucket", - "revision": "7276be0fdf719ddff753f3574ef0f967e4a5a5f7", - "revisionTime": "2017-02-20T14:02:47Z" + "revision": "90362394a367f9d77730911973462a53d69662ba", + "revisionTime": "2017-02-23T14:12:10Z" }, { "checksumSHA1": "1Kp4DKkJNVn135Xg8H4a6CFBNy8=", "path": "github.com/markbates/goth/providers/dropbox", - "revision": "7276be0fdf719ddff753f3574ef0f967e4a5a5f7", - "revisionTime": "2017-02-20T14:02:47Z" + "revision": "90362394a367f9d77730911973462a53d69662ba", + "revisionTime": "2017-02-23T14:12:10Z" }, { "checksumSHA1": "cGs1da29iOBJh5EAH0icKDbN8CA=", "path": "github.com/markbates/goth/providers/facebook", - "revision": "7276be0fdf719ddff753f3574ef0f967e4a5a5f7", - "revisionTime": "2017-02-20T14:02:47Z" + "revision": "90362394a367f9d77730911973462a53d69662ba", + "revisionTime": "2017-02-23T14:12:10Z" }, { - "checksumSHA1": "ZFqznX3/ZW65I4QeepiHQdE69nA=", + "checksumSHA1": "P6nBZ850aaekpOcoXNdRhK86bH8=", "path": "github.com/markbates/goth/providers/github", - "revision": "450379d2950a65070b23cc93c53436553add4484", - "revisionTime": "2017-02-06T19:46:32Z" + "revision": "90362394a367f9d77730911973462a53d69662ba", + "revisionTime": "2017-02-23T14:12:10Z" }, { - "checksumSHA1": "QO8bvOenTBbLPzr3ZvB7LCHJ0PY=", + "checksumSHA1": "o/109paSRy9HqV87gR4zUZMMSzs=", "path": "github.com/markbates/goth/providers/gitlab", - "revision": "a0a751ca505adde67bc2c92e2aae99531b1e3213", - "revisionTime": "2017-02-20T13:56:36Z" + "revision": "90362394a367f9d77730911973462a53d69662ba", + "revisionTime": "2017-02-23T14:12:10Z" }, { "checksumSHA1": "cX6kR9y94BWFZvI/7UFrsFsP3FQ=", "path": "github.com/markbates/goth/providers/gplus", - "revision": "a0a751ca505adde67bc2c92e2aae99531b1e3213", - "revisionTime": "2017-02-20T13:56:36Z" + "revision": "90362394a367f9d77730911973462a53d69662ba", + "revisionTime": "2017-02-23T14:12:10Z" }, { "checksumSHA1": "sMYKhqAUZXM1+T/TjlMhWh8Vveo=", "path": "github.com/markbates/goth/providers/openidConnect", - "revision": "7276be0fdf719ddff753f3574ef0f967e4a5a5f7", - "revisionTime": "2017-02-20T14:02:47Z" + "revision": "90362394a367f9d77730911973462a53d69662ba", + "revisionTime": "2017-02-23T14:12:10Z" }, { "checksumSHA1": "1w0V6jYXaGlEtZcMeYTOAAucvgw=", "path": "github.com/markbates/goth/providers/twitter", - "revision": "7276be0fdf719ddff753f3574ef0f967e4a5a5f7", - "revisionTime": "2017-02-20T14:02:47Z" + "revision": "90362394a367f9d77730911973462a53d69662ba", + "revisionTime": "2017-02-23T14:12:10Z" }, { "checksumSHA1": "9FJUwn3EIgASVki+p8IHgWVC5vQ=", From ab95945567962f81033092bcf5b6cd1ae908c8ac Mon Sep 17 00:00:00 2001 From: willemvd Date: Fri, 24 Feb 2017 16:33:41 +0100 Subject: [PATCH 13/19] split up oauth2 into multiple files --- models/error.go | 25 --------- models/error_oauth2.go | 24 ++++++++ models/login_source.go | 121 +--------------------------------------- models/oauth2.go | 122 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+), 145 deletions(-) create mode 100644 models/error_oauth2.go create mode 100644 models/oauth2.go diff --git a/models/error.go b/models/error.go index 7382c60256082..472c8c94266a5 100644 --- a/models/error.go +++ b/models/error.go @@ -887,28 +887,3 @@ func IsErrExternalLoginUserNotExist(err error) bool { func (err ErrExternalLoginUserNotExist) Error() string { return fmt.Sprintf("external login user link does not exists [userID: %d, loginSourceID: %d]", err.UserID, err.LoginSourceID) } - -// ________ _____ ____ _________________ ___ -// \_____ \ / _ \ | | \__ ___/ | \ -// / | \ / /_\ \| | / | | / ~ \ -// / | \/ | \ | / | | \ Y / -// \_______ /\____|__ /______/ |____| \___|_ / -// \/ \/ \/ - - -// ErrOpenIDConnectInitialize represents a "OpenIDConnectInitialize" kind of error. -type ErrOpenIDConnectInitialize struct { - OpenIDConnectAutoDiscoveryURL string - ProviderName string - Cause error -} - -// IsErrOpenIDConnectInitialize checks if an error is a ExternalLoginUserAlreadyExist. -func IsErrOpenIDConnectInitialize(err error) bool { - _, ok := err.(ErrOpenIDConnectInitialize) - return ok -} - -func (err ErrOpenIDConnectInitialize) Error() string { - return fmt.Sprintf("Failed to initialize OpenID Connect Provider with name '%s' with url '%s': %v", err.ProviderName, err.OpenIDConnectAutoDiscoveryURL, err.Cause) -} \ No newline at end of file diff --git a/models/error_oauth2.go b/models/error_oauth2.go new file mode 100644 index 0000000000000..94c68a2c12c9e --- /dev/null +++ b/models/error_oauth2.go @@ -0,0 +1,24 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import "fmt" + +// ErrOpenIDConnectInitialize represents a "OpenIDConnectInitialize" kind of error. +type ErrOpenIDConnectInitialize struct { + OpenIDConnectAutoDiscoveryURL string + ProviderName string + Cause error +} + +// IsErrOpenIDConnectInitialize checks if an error is a ExternalLoginUserAlreadyExist. +func IsErrOpenIDConnectInitialize(err error) bool { + _, ok := err.(ErrOpenIDConnectInitialize) + return ok +} + +func (err ErrOpenIDConnectInitialize) Error() string { + return fmt.Sprintf("Failed to initialize OpenID Connect Provider with name '%s' with url '%s': %v", err.ProviderName, err.OpenIDConnectAutoDiscoveryURL, err.Cause) +} diff --git a/models/login_source.go b/models/login_source.go index 7b422ed57be87..211a246da297f 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -11,7 +11,6 @@ import ( "fmt" "net/smtp" "net/textproto" - "sort" "strings" "time" @@ -602,56 +601,6 @@ func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMCon return user, CreateUser(user) } -// ________ _____ __ .__ ________ -// \_____ \ / _ \ __ ___/ |_| |__ \_____ \ -// / | \ / /_\ \| | \ __\ | \ / ____/ -// / | \/ | \ | /| | | Y \/ \ -// \_______ /\____|__ /____/ |__| |___| /\_______ \ -// \/ \/ \/ \/ - -// OAuth2Provider describes the display values of a single OAuth2 provider -type OAuth2Provider struct { - Name string - DisplayName string - Image string - CustomURLMapping *oauth2.CustomURLMapping -} - -// OAuth2Providers contains the map of registered OAuth2 providers in Gitea (based on goth) -// key is used to map the OAuth2Provider with the goth provider type (also in LoginSource.OAuth2Config.Provider) -// value is used to store display data -var OAuth2Providers = map[string]OAuth2Provider{ - "bitbucket": {Name: "bitbucket", DisplayName: "Bitbucket", Image: "/img/auth/bitbucket.png"}, - "dropbox": {Name: "dropbox", DisplayName: "Dropbox", Image: "/img/auth/dropbox.png"}, - "facebook": {Name: "facebook", DisplayName: "Facebook", Image: "/img/auth/facebook.png"}, - "github": {Name: "github", DisplayName: "GitHub", Image: "/img/auth/github.png", - CustomURLMapping: &oauth2.CustomURLMapping{ - TokenURL: oauth2.GetDefaultTokenURL("github"), - AuthURL: oauth2.GetDefaultAuthURL("github"), - ProfileURL: oauth2.GetDefaultProfileURL("github"), - EmailURL: oauth2.GetDefaultEmailURL("github"), - }, - }, - "gitlab": {Name: "gitlab", DisplayName: "GitLab", Image: "/img/auth/gitlab.png", - CustomURLMapping: &oauth2.CustomURLMapping{ - TokenURL: oauth2.GetDefaultTokenURL("gitlab"), - AuthURL: oauth2.GetDefaultAuthURL("gitlab"), - ProfileURL: oauth2.GetDefaultProfileURL("gitlab"), - }, - }, - "gplus": {Name: "gplus", DisplayName: "Google+", Image: "/img/auth/google_plus.png"}, - "openidConnect": {Name: "openidConnect", DisplayName: "OpenID Connect", Image: "/img/auth/openid_connect.png"}, - "twitter": {Name: "twitter", DisplayName: "Twitter", Image: "/img/auth/twitter.png"}, -} - -// OAuth2DefaultCustomURLMappings contains the map of default URL's for OAuth2 providers that are allowed to have custom urls -// key is used to map the OAuth2Provider -// value is the mapping as defined for the OAuth2Provider -var OAuth2DefaultCustomURLMappings = map[string]*oauth2.CustomURLMapping { - "github": OAuth2Providers["github"].CustomURLMapping, - "gitlab": OAuth2Providers["gitlab"].CustomURLMapping, -} - // ExternalUserLogin attempts a login using external source types. func ExternalUserLogin(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) { if !source.IsActived { @@ -725,72 +674,4 @@ func UserSignIn(username, password string) (*User, error) { } return nil, ErrUserNotExist{user.ID, user.Name, 0} -} - -// GetActiveOAuth2ProviderLoginSources returns all actived LoginOAuth2 sources -func GetActiveOAuth2ProviderLoginSources() ([]*LoginSource, error) { - sources := make([]*LoginSource, 0, 1) - if err := x.UseBool().Find(&sources, &LoginSource{IsActived: true, Type: LoginOAuth2}); err != nil { - return nil, err - } - return sources, nil -} - -// GetActiveOAuth2LoginSourceByName returns a OAuth2 LoginSource based on the given name -func GetActiveOAuth2LoginSourceByName(name string) (*LoginSource, error) { - loginSource := &LoginSource{ - Name: name, - Type: LoginOAuth2, - IsActived: true, - } - - has, err := x.UseBool().Get(loginSource) - if !has || err != nil { - return nil, err - } - - return loginSource, nil -} - -// GetActiveOAuth2Providers returns the map of configured active OAuth2 providers -// key is used as technical name (like in the callbackURL) -// values to display -func GetActiveOAuth2Providers() ([]string, map[string]OAuth2Provider, error) { - // Maybe also seperate used and unused providers so we can force the registration of only 1 active provider for each type - - loginSources, err := GetActiveOAuth2ProviderLoginSources() - if err != nil { - return nil, nil, err - } - - var orderedKeys []string - providers := make(map[string]OAuth2Provider) - for _, source := range loginSources { - providers[source.Name] = OAuth2Providers[source.OAuth2().Provider] - orderedKeys = append(orderedKeys, source.Name) - } - - sort.Strings(orderedKeys) - - return orderedKeys, providers, nil -} - -// InitOAuth2 initialize the OAuth2 lib and register all active OAuth2 providers in the library -func InitOAuth2() { - oauth2.Init() - loginSources, _ := GetActiveOAuth2ProviderLoginSources() - - for _, source := range loginSources { - oAuth2Config := source.OAuth2() - oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL, oAuth2Config.CustomURLMapping) - } -} - -// wrapOpenIDConnectInitializeError is used to wrap the error but this cannot be done in modules/auth/oauth2 -// inside oauth2: import cycle not allowed models -> modules/auth/oauth2 -> models -func wrapOpenIDConnectInitializeError(err error, providerName string, oAuth2Config *OAuth2Config) error { - if err != nil && "openidConnect" == oAuth2Config.Provider { - err = ErrOpenIDConnectInitialize{ProviderName: providerName, OpenIDConnectAutoDiscoveryURL: oAuth2Config.OpenIDConnectAutoDiscoveryURL, Cause: err} - } - return err -} +} \ No newline at end of file diff --git a/models/oauth2.go b/models/oauth2.go new file mode 100644 index 0000000000000..884c22fb25496 --- /dev/null +++ b/models/oauth2.go @@ -0,0 +1,122 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "sort" + "code.gitea.io/gitea/modules/auth/oauth2" +) + +// OAuth2Provider describes the display values of a single OAuth2 provider +type OAuth2Provider struct { + Name string + DisplayName string + Image string + CustomURLMapping *oauth2.CustomURLMapping +} + +// OAuth2Providers contains the map of registered OAuth2 providers in Gitea (based on goth) +// key is used to map the OAuth2Provider with the goth provider type (also in LoginSource.OAuth2Config.Provider) +// value is used to store display data +var OAuth2Providers = map[string]OAuth2Provider{ + "bitbucket": {Name: "bitbucket", DisplayName: "Bitbucket", Image: "/img/auth/bitbucket.png"}, + "dropbox": {Name: "dropbox", DisplayName: "Dropbox", Image: "/img/auth/dropbox.png"}, + "facebook": {Name: "facebook", DisplayName: "Facebook", Image: "/img/auth/facebook.png"}, + "github": {Name: "github", DisplayName: "GitHub", Image: "/img/auth/github.png", + CustomURLMapping: &oauth2.CustomURLMapping{ + TokenURL: oauth2.GetDefaultTokenURL("github"), + AuthURL: oauth2.GetDefaultAuthURL("github"), + ProfileURL: oauth2.GetDefaultProfileURL("github"), + EmailURL: oauth2.GetDefaultEmailURL("github"), + }, + }, + "gitlab": {Name: "gitlab", DisplayName: "GitLab", Image: "/img/auth/gitlab.png", + CustomURLMapping: &oauth2.CustomURLMapping{ + TokenURL: oauth2.GetDefaultTokenURL("gitlab"), + AuthURL: oauth2.GetDefaultAuthURL("gitlab"), + ProfileURL: oauth2.GetDefaultProfileURL("gitlab"), + }, + }, + "gplus": {Name: "gplus", DisplayName: "Google+", Image: "/img/auth/google_plus.png"}, + "openidConnect": {Name: "openidConnect", DisplayName: "OpenID Connect", Image: "/img/auth/openid_connect.png"}, + "twitter": {Name: "twitter", DisplayName: "Twitter", Image: "/img/auth/twitter.png"}, +} + +// OAuth2DefaultCustomURLMappings contains the map of default URL's for OAuth2 providers that are allowed to have custom urls +// key is used to map the OAuth2Provider +// value is the mapping as defined for the OAuth2Provider +var OAuth2DefaultCustomURLMappings = map[string]*oauth2.CustomURLMapping { + "github": OAuth2Providers["github"].CustomURLMapping, + "gitlab": OAuth2Providers["gitlab"].CustomURLMapping, +} + +// GetActiveOAuth2ProviderLoginSources returns all actived LoginOAuth2 sources +func GetActiveOAuth2ProviderLoginSources() ([]*LoginSource, error) { + sources := make([]*LoginSource, 0, 1) + if err := x.UseBool().Find(&sources, &LoginSource{IsActived: true, Type: LoginOAuth2}); err != nil { + return nil, err + } + return sources, nil +} + +// GetActiveOAuth2LoginSourceByName returns a OAuth2 LoginSource based on the given name +func GetActiveOAuth2LoginSourceByName(name string) (*LoginSource, error) { + loginSource := &LoginSource{ + Name: name, + Type: LoginOAuth2, + IsActived: true, + } + + has, err := x.UseBool().Get(loginSource) + if !has || err != nil { + return nil, err + } + + return loginSource, nil +} + +// GetActiveOAuth2Providers returns the map of configured active OAuth2 providers +// key is used as technical name (like in the callbackURL) +// values to display +func GetActiveOAuth2Providers() ([]string, map[string]OAuth2Provider, error) { + // Maybe also seperate used and unused providers so we can force the registration of only 1 active provider for each type + + loginSources, err := GetActiveOAuth2ProviderLoginSources() + if err != nil { + return nil, nil, err + } + + var orderedKeys []string + providers := make(map[string]OAuth2Provider) + for _, source := range loginSources { + providers[source.Name] = OAuth2Providers[source.OAuth2().Provider] + orderedKeys = append(orderedKeys, source.Name) + } + + sort.Strings(orderedKeys) + + return orderedKeys, providers, nil +} + +// InitOAuth2 initialize the OAuth2 lib and register all active OAuth2 providers in the library +func InitOAuth2() { + oauth2.Init() + loginSources, _ := GetActiveOAuth2ProviderLoginSources() + + for _, source := range loginSources { + oAuth2Config := source.OAuth2() + oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL, oAuth2Config.CustomURLMapping) + } +} + +// wrapOpenIDConnectInitializeError is used to wrap the error but this cannot be done in modules/auth/oauth2 +// inside oauth2: import cycle not allowed models -> modules/auth/oauth2 -> models +func wrapOpenIDConnectInitializeError(err error, providerName string, oAuth2Config *OAuth2Config) error { + if err != nil && "openidConnect" == oAuth2Config.Provider { + err = ErrOpenIDConnectInitialize{ProviderName: providerName, OpenIDConnectAutoDiscoveryURL: oAuth2Config.OpenIDConnectAutoDiscoveryURL, Cause: err} + } + return err +} + From 6aeea3f5591c4b72256291c23394c1b752e2524e Mon Sep 17 00:00:00 2001 From: willemvd Date: Fri, 10 Mar 2017 08:28:04 +0100 Subject: [PATCH 14/19] small typo in comment --- models/oauth2.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/oauth2.go b/models/oauth2.go index 884c22fb25496..9de64a05877c6 100644 --- a/models/oauth2.go +++ b/models/oauth2.go @@ -81,7 +81,7 @@ func GetActiveOAuth2LoginSourceByName(name string) (*LoginSource, error) { // key is used as technical name (like in the callbackURL) // values to display func GetActiveOAuth2Providers() ([]string, map[string]OAuth2Provider, error) { - // Maybe also seperate used and unused providers so we can force the registration of only 1 active provider for each type + // Maybe also separate used and unused providers so we can force the registration of only 1 active provider for each type loginSources, err := GetActiveOAuth2ProviderLoginSources() if err != nil { From 92e802d442dac67bf6d888019e53d902e2bd4b29 Mon Sep 17 00:00:00 2001 From: willemvd Date: Tue, 28 Mar 2017 11:07:14 +0200 Subject: [PATCH 15/19] fix indention --- templates/admin/auth/new.tmpl | 54 +++++++++++++++++------------------ 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl index 3ffb670e606ea..00239b0462c59 100644 --- a/templates/admin/auth/new.tmpl +++ b/templates/admin/auth/new.tmpl @@ -74,36 +74,34 @@
    -

    - {{.i18n.Tr "admin.auths.tips"}} -

    -
    -
    GMail Settings:
    -

    Host: smtp.gmail.com, Port: 587, Enable TLS Encryption: true

    +

    + {{.i18n.Tr "admin.auths.tips"}} +

    +
    +
    GMail Settings:
    +

    Host: smtp.gmail.com, Port: 587, Enable TLS Encryption: true

    -
    {{.i18n.Tr "admin.auths.tips.oauth2.general"}}:
    -

    {{.i18n.Tr "admin.auths.tips.oauth2.general.tip"}}

    +
    {{.i18n.Tr "admin.auths.tips.oauth2.general"}}:
    +

    {{.i18n.Tr "admin.auths.tips.oauth2.general.tip"}}

    -
    {{.i18n.Tr "admin.auths.tip.oauth2_provider"}}
    -
    -
  • Bitbucket
  • - {{.i18n.Tr "admin.auths.tip.bitbucket"}} -
  • Dropbox
  • - {{.i18n.Tr "admin.auths.tip.dropbox"}} -
  • Facebook
  • - {{.i18n.Tr "admin.auths.tip.facebook"}} -
  • GitHub
  • - {{.i18n.Tr "admin.auths.tip.github"}} -
  • GitLab
  • - {{.i18n.Tr "admin.auths.tip.gitlab"}} -
  • Google+
  • - {{.i18n.Tr "admin.auths.tip.google_plus"}} -
  • OpenID Connect
  • - {{.i18n.Tr "admin.auths.tip.openid_connect"}} -
  • Twitter
  • - {{.i18n.Tr "admin.auths.tip.twitter"}} -
    -
    +
    {{.i18n.Tr "admin.auths.tip.oauth2_provider"}}
    +
    +
  • Bitbucket
  • + {{.i18n.Tr "admin.auths.tip.bitbucket"}} +
  • Dropbox
  • + {{.i18n.Tr "admin.auths.tip.dropbox"}} +
  • Facebook
  • + {{.i18n.Tr "admin.auths.tip.facebook"}} +
  • GitHub
  • + {{.i18n.Tr "admin.auths.tip.github"}} +
  • GitLab
  • + {{.i18n.Tr "admin.auths.tip.gitlab"}} +
  • Google+
  • + {{.i18n.Tr "admin.auths.tip.google_plus"}} +
  • OpenID Connect
  • + {{.i18n.Tr "admin.auths.tip.openid_connect"}} +
  • Twitter
  • + {{.i18n.Tr "admin.auths.tip.twitter"}}
    From 5f0067e9603b951eaafc37db10b2114bec415547 Mon Sep 17 00:00:00 2001 From: willemvd Date: Fri, 28 Apr 2017 11:33:43 +0200 Subject: [PATCH 16/19] fix indentation --- templates/admin/auth/edit.tmpl | 112 ++++++++++++++++----------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index 97b6a6d96a123..3c74b2ad17538 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -140,64 +140,64 @@ {{end}} - - {{if .Source.IsOAuth2}} - {{ $cfg:=.Source.OAuth2 }} -
    - - -
    -
    - - -
    -
    - - -
    -
    - - + + {{if .Source.IsOAuth2}} + {{ $cfg:=.Source.OAuth2 }} +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    -
    -
    - - -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    - {{if .OAuth2DefaultCustomURLMappings}}{{range $key, $value := .OAuth2DefaultCustomURLMappings}} - - - - - {{end}}{{end}} - {{end}} +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + {{if .OAuth2DefaultCustomURLMappings}}{{range $key, $value := .OAuth2DefaultCustomURLMappings}} + + + + + {{end}}{{end}} + {{end}}
    From 922c5d42e563cab5073ae683474bed6b98ed0257 Mon Sep 17 00:00:00 2001 From: willemvd Date: Fri, 28 Apr 2017 11:34:00 +0200 Subject: [PATCH 17/19] fix new line before external import --- routers/admin/auths.go | 1 + 1 file changed, 1 insertion(+) diff --git a/routers/admin/auths.go b/routers/admin/auths.go index 4d3ecac12f40e..eb7c7e8e93967 100644 --- a/routers/admin/auths.go +++ b/routers/admin/auths.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "github.com/Unknwon/com" "github.com/go-xorm/core" ) From e18dfe5b41b01e7529e1cb94cf9f7cbaa91104ed Mon Sep 17 00:00:00 2001 From: willemvd Date: Fri, 28 Apr 2017 13:41:32 +0200 Subject: [PATCH 18/19] fix layout of signin part --- templates/user/auth/link_account.tmpl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/user/auth/link_account.tmpl b/templates/user/auth/link_account.tmpl index 5dc54ca5a4d26..73aafd6feb804 100644 --- a/templates/user/auth/link_account.tmpl +++ b/templates/user/auth/link_account.tmpl @@ -8,6 +8,8 @@
    + {{template "user/auth/signup_inner" .}} {{template "base/footer" .}} From fe450e28d048d5044215c4b6d7bf22f8c9b4fdaf Mon Sep 17 00:00:00 2001 From: willemvd Date: Mon, 1 May 2017 08:09:39 +0200 Subject: [PATCH 19/19] update "broken" dependency --- vendor/github.com/mrjones/oauth/README.md | 51 +++++++++++++++++++ vendor/github.com/mrjones/oauth/oauth.go | 6 +++ vendor/github.com/mrjones/oauth/pre-commit.sh | 0 vendor/github.com/mrjones/oauth/provider.go | 7 +++ vendor/vendor.json | 6 +-- 5 files changed, 67 insertions(+), 3 deletions(-) mode change 100644 => 100755 vendor/github.com/mrjones/oauth/pre-commit.sh diff --git a/vendor/github.com/mrjones/oauth/README.md b/vendor/github.com/mrjones/oauth/README.md index e69de29bb2d1d..c0f7eb5479b94 100644 --- a/vendor/github.com/mrjones/oauth/README.md +++ b/vendor/github.com/mrjones/oauth/README.md @@ -0,0 +1,51 @@ +OAuth 1.0 Library for [Go](http://golang.org) +======================== + +[![GoDoc](http://godoc.org/github.com/mrjones/oauth?status.png)](http://godoc.org/github.com/mrjones/oauth) + +[![CircleCI](https://circleci.com/gh/mrjones/oauth/tree/master.svg?style=svg)](https://circleci.com/gh/mrjones/oauth/tree/master) + +(If you need an OAuth 2.0 library, check out: https://godoc.org/golang.org/x/oauth2) + +Developing your own apps, with this library +------------------------------------------- + +* First, install the library + + go get github.com/mrjones/oauth + +* Then, check out the comments in oauth.go + +* Or, have a look at the examples: + + * Netflix + + go run examples/netflix/netflix.go --consumerkey [key] --consumersecret [secret] --appname [appname] + + * Twitter + + Command line: + + go run examples/twitter/twitter.go --consumerkey [key] --consumersecret [secret] + + Or, in the browser (using an HTTP server): + + go run examples/twitterserver/twitterserver.go --consumerkey [key] --consumersecret [secret] --port 8888 + + * The Google Latitude example is broken, now that Google uses OAuth 2.0 + +Contributing to this library +---------------------------- + +* Please install the pre-commit hook, which will run tests, and go-fmt before committing. + + ln -s $PWD/pre-commit.sh .git/hooks/pre-commit + +* Running tests and building is as you'd expect: + + go test *.go + go build *.go + + + + diff --git a/vendor/github.com/mrjones/oauth/oauth.go b/vendor/github.com/mrjones/oauth/oauth.go index 6ae39f2376a57..95eee64abd6ed 100644 --- a/vendor/github.com/mrjones/oauth/oauth.go +++ b/vendor/github.com/mrjones/oauth/oauth.go @@ -137,6 +137,12 @@ type ServiceProvider struct { HttpMethod string BodyHash bool IgnoreTimestamp bool + + // Enables non spec-compliant behavior: + // Allow parameters to be passed in the query string rather + // than the body. + // See https://github.com/mrjones/oauth/pull/63 + SignQueryParams bool } func (sp *ServiceProvider) httpMethod() string { diff --git a/vendor/github.com/mrjones/oauth/pre-commit.sh b/vendor/github.com/mrjones/oauth/pre-commit.sh old mode 100644 new mode 100755 diff --git a/vendor/github.com/mrjones/oauth/provider.go b/vendor/github.com/mrjones/oauth/provider.go index da72fba3afec4..3a37e38a1bd44 100644 --- a/vendor/github.com/mrjones/oauth/provider.go +++ b/vendor/github.com/mrjones/oauth/provider.go @@ -124,6 +124,13 @@ func (provider *Provider) IsAuthorized(request *http.Request) (*string, error) { } } + // Include the query string params in the base string + if consumer.serviceProvider.SignQueryParams { + for k, v := range request.URL.Query() { + userParams[k] = strings.Join(v, "") + } + } + // if our consumer supports bodyhash, check it if consumer.serviceProvider.BodyHash { bodyHash, err := calculateBodyHash(request, consumer.signer) diff --git a/vendor/vendor.json b/vendor/vendor.json index 12a94214efa92..9ee35e5f3fbc2 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -618,10 +618,10 @@ "revisionTime": "2016-10-12T08:37:05Z" }, { - "checksumSHA1": "/le/3bmROVfXmIQT9CbS22aqdcE=", + "checksumSHA1": "hQcIDtbilIlkJaYhl2faWIFL8uY=", "path": "github.com/mrjones/oauth", - "revision": "1359071b7221e8f5ffa3985727f51336e16f741a", - "revisionTime": "2017-01-08T19:16:49Z" + "revision": "3f67d9c274355678b2f9844b08d643e2f9213340", + "revisionTime": "2017-02-25T17:57:52Z" }, { "checksumSHA1": "lfOuMiAdiqc/dalUSBTvD5ZMSzA=",