Additional OAuth2 providers (#1010)
* add google+ * sort signin oauth2 providers based on the name so order is always the same * update auth tip for google+ * add gitlab provider * add bitbucket provider (and some go fmt) * add twitter provider * add facebook provider * add dropbox provider * add openid connect provider incl. new format of tips section in "Add New Source" * lower the amount of disk storage for each session to prevent issues while building cross platform (and disk overflow) * imports according to goimport and code style * make it possible to set custom urls to gitlab and github provider (only these could have a different host) * split up oauth2 into multiple files * small typo in comment * fix indention * fix indentation * fix new line before external import * fix layout of signin part * update "broken" dependency
|
@ -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)
|
||||||
|
}
|
|
@ -124,6 +124,8 @@ type OAuth2Config struct {
|
||||||
Provider string
|
Provider string
|
||||||
ClientID string
|
ClientID string
|
||||||
ClientSecret string
|
ClientSecret string
|
||||||
|
OpenIDConnectAutoDiscoveryURL string
|
||||||
|
CustomURLMapping *oauth2.CustomURLMapping
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromDB fills up an OAuth2Config from serialized format.
|
// FromDB fills up an OAuth2Config from serialized format.
|
||||||
|
@ -294,9 +296,15 @@ func CreateLoginSource(source *LoginSource) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = x.Insert(source)
|
_, err = x.Insert(source)
|
||||||
if err == nil && source.IsOAuth2() {
|
if err == nil && source.IsOAuth2() && source.IsActived {
|
||||||
oAuth2Config := source.OAuth2()
|
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, oAuth2Config.CustomURLMapping)
|
||||||
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
@ -321,11 +329,25 @@ func GetLoginSourceByID(id int64) (*LoginSource, error) {
|
||||||
|
|
||||||
// UpdateSource updates a LoginSource record in DB.
|
// UpdateSource updates a LoginSource record in DB.
|
||||||
func UpdateSource(source *LoginSource) error {
|
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)
|
_, err := x.Id(source.ID).AllCols().Update(source)
|
||||||
if err == nil && source.IsOAuth2() {
|
if err == nil && source.IsOAuth2() && source.IsActived {
|
||||||
oAuth2Config := source.OAuth2()
|
oAuth2Config := source.OAuth2()
|
||||||
oauth2.RemoveProvider(source.Name)
|
err = oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL, oAuth2Config.CustomURLMapping)
|
||||||
oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret)
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
@ -580,27 +602,6 @@ func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMCon
|
||||||
return user, CreateUser(user)
|
return user, CreateUser(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ________ _____ __ .__ ________
|
|
||||||
// \_____ \ / _ \ __ ___/ |_| |__ \_____ \
|
|
||||||
// / | \ / /_\ \| | \ __\ | \ / ____/
|
|
||||||
// / | \/ | \ | /| | | Y \/ \
|
|
||||||
// \_______ /\____|__ /____/ |__| |___| /\_______ \
|
|
||||||
// \/ \/ \/ \/
|
|
||||||
|
|
||||||
// OAuth2Provider describes the display values of a single OAuth2 provider
|
|
||||||
type OAuth2Provider struct {
|
|
||||||
Name string
|
|
||||||
DisplayName 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/github.png"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExternalUserLogin attempts a login using external source types.
|
// ExternalUserLogin attempts a login using external source types.
|
||||||
func ExternalUserLogin(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) {
|
func ExternalUserLogin(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) {
|
||||||
if !source.IsActived {
|
if !source.IsActived {
|
||||||
|
@ -685,58 +686,3 @@ func UserSignIn(username, password string) (*User, error) {
|
||||||
|
|
||||||
return nil, ErrUserNotExist{user.ID, user.Name, 0}
|
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() (map[string]OAuth2Provider, error) {
|
|
||||||
// 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 {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
providers := make(map[string]OAuth2Provider)
|
|
||||||
for _, source := range loginSources {
|
|
||||||
providers[source.Name] = OAuth2Providers[source.OAuth2().Provider]
|
|
||||||
}
|
|
||||||
|
|
||||||
return 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -39,6 +39,12 @@ type AuthenticationForm struct {
|
||||||
Oauth2Provider string
|
Oauth2Provider string
|
||||||
Oauth2Key string
|
Oauth2Key string
|
||||||
Oauth2Secret string
|
Oauth2Secret string
|
||||||
|
OpenIDConnectAutoDiscoveryURL string
|
||||||
|
Oauth2UseCustomURL bool
|
||||||
|
Oauth2TokenURL string
|
||||||
|
Oauth2AuthURL string
|
||||||
|
Oauth2ProfileURL string
|
||||||
|
Oauth2EmailURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates fields
|
// Validate validates fields
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
package oauth2
|
package oauth2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -15,7 +16,14 @@ import (
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/markbates/goth"
|
"github.com/markbates/goth"
|
||||||
"github.com/markbates/goth/gothic"
|
"github.com/markbates/goth/gothic"
|
||||||
|
"github.com/markbates/goth/providers/bitbucket"
|
||||||
|
"github.com/markbates/goth/providers/dropbox"
|
||||||
|
"github.com/markbates/goth/providers/facebook"
|
||||||
"github.com/markbates/goth/providers/github"
|
"github.com/markbates/goth/providers/github"
|
||||||
|
"github.com/markbates/goth/providers/gitlab"
|
||||||
|
"github.com/markbates/goth/providers/gplus"
|
||||||
|
"github.com/markbates/goth/providers/openidConnect"
|
||||||
|
"github.com/markbates/goth/providers/twitter"
|
||||||
"github.com/satori/go.uuid"
|
"github.com/satori/go.uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,6 +32,14 @@ var (
|
||||||
providerHeaderKey = "gitea-oauth2-provider"
|
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
|
// Init initialize the setup of the OAuth2 library
|
||||||
func Init() {
|
func Init() {
|
||||||
sessionDir := filepath.Join(setting.AppDataPath, "sessions", "oauth2")
|
sessionDir := filepath.Join(setting.AppDataPath, "sessions", "oauth2")
|
||||||
|
@ -31,7 +47,15 @@ func Init() {
|
||||||
log.Fatal(4, "Fail to create dir %s: %v", sessionDir, err)
|
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.MaxInt16)
|
||||||
|
gothic.Store = store
|
||||||
|
|
||||||
gothic.SetState = func(req *http.Request) string {
|
gothic.SetState = func(req *http.Request) string {
|
||||||
return uuid.NewV4().String()
|
return uuid.NewV4().String()
|
||||||
|
@ -74,12 +98,14 @@ func ProviderCallback(provider string, request *http.Request, response http.Resp
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterProvider register a OAuth2 provider in goth lib
|
// RegisterProvider register a OAuth2 provider in goth lib
|
||||||
func RegisterProvider(providerName, providerType, clientID, clientSecret string) {
|
func RegisterProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL string, customURLMapping *CustomURLMapping) error {
|
||||||
provider := createProvider(providerName, providerType, clientID, clientSecret)
|
provider, err := createProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL, customURLMapping)
|
||||||
|
|
||||||
if provider != nil {
|
if err == nil && provider != nil {
|
||||||
goth.UseProviders(provider)
|
goth.UseProviders(provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveProvider removes the given OAuth2 provider from the goth lib
|
// RemoveProvider removes the given OAuth2 provider from the goth lib
|
||||||
|
@ -88,20 +114,111 @@ func RemoveProvider(providerName string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// used to create different types of goth providers
|
// 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, customURLMapping *CustomURLMapping) (goth.Provider, error) {
|
||||||
callbackURL := setting.AppURL + "user/oauth2/" + providerName + "/callback"
|
callbackURL := setting.AppURL + "user/oauth2/" + providerName + "/callback"
|
||||||
|
|
||||||
var provider goth.Provider
|
var provider goth.Provider
|
||||||
|
var err error
|
||||||
|
|
||||||
switch providerType {
|
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":
|
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":
|
||||||
|
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":
|
||||||
|
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
|
// 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)
|
provider.SetName(providerName)
|
||||||
}
|
}
|
||||||
|
|
||||||
return provider
|
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 ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -1171,12 +1171,28 @@ auths.allowed_domains_helper = Leave it empty to not restrict any domains. Multi
|
||||||
auths.enable_tls = Enable TLS Encryption
|
auths.enable_tls = Enable TLS Encryption
|
||||||
auths.skip_tls_verify = Skip TLS Verify
|
auths.skip_tls_verify = Skip TLS Verify
|
||||||
auths.pam_service_name = PAM Service Name
|
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_clientID = Client ID (Key)
|
||||||
auths.oauth2_clientSecret = Client Secret
|
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.enable_auto_register = Enable Auto Registration
|
||||||
auths.tips = Tips
|
auths.tips = Tips
|
||||||
auths.tip.github = Register a new OAuth application on https://github.com/settings/applications/new and use <host>/user/oauth2/<Authentication Name>/callback as "Authorization callback URL"
|
auths.tips.oauth2.general = OAuth2 Authentication
|
||||||
|
auths.tips.oauth2.general.tip = When registering a new OAuth2 authentication, the callback/redirect URL should be: <host>/user/oauth2/<Authentication Name>/callback
|
||||||
|
auths.tip.oauth2_provider = OAuth2 Provider
|
||||||
|
auths.tip.bitbucket = Register a new OAuth consumer on https://bitbucket.org/account/user/<your username>/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 (<server>/.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.edit = Edit Authentication Setting
|
||||||
auths.activated = This authentication is activated
|
auths.activated = This authentication is activated
|
||||||
auths.new_success = New authentication '%s' has been added successfully.
|
auths.new_success = New authentication '%s' has been added successfully.
|
||||||
|
|
|
@ -965,7 +965,7 @@ footer .ui.language .menu {
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
.signin .oauth2 a {
|
.signin .oauth2 a {
|
||||||
margin-right: 5px;
|
margin-right: 3px;
|
||||||
}
|
}
|
||||||
.signin .oauth2 a:last-child {
|
.signin .oauth2 a:last-child {
|
||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
|
@ -974,6 +974,9 @@ footer .ui.language .menu {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
.signin .oauth2 img.openidConnect {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
.user.activate form,
|
.user.activate form,
|
||||||
.user.forgot.password form,
|
.user.forgot.password form,
|
||||||
.user.reset.password form,
|
.user.reset.password form,
|
||||||
|
|
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 3.0 KiB |
|
@ -1047,6 +1047,56 @@ 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();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// New authentication
|
// New authentication
|
||||||
if ($('.admin.new.authentication').length > 0) {
|
if ($('.admin.new.authentication').length > 0) {
|
||||||
$('#auth_type').change(function () {
|
$('#auth_type').change(function () {
|
||||||
|
@ -1075,22 +1125,28 @@ function initAdmin() {
|
||||||
break;
|
break;
|
||||||
case '6': // OAuth2
|
case '6': // OAuth2
|
||||||
$('.oauth2').show();
|
$('.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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authType == '2' || authType == '5') {
|
if (authType == '2' || authType == '5') {
|
||||||
onSecurityProtocolChange()
|
onSecurityProtocolChange()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$('#auth_type').change();
|
$('#auth_type').change();
|
||||||
$('#security_protocol').change(onSecurityProtocolChange)
|
$('#security_protocol').change(onSecurityProtocolChange);
|
||||||
|
$('#oauth2_provider').change(onOAuth2Change);
|
||||||
|
$('#oauth2_use_custom_url').change(onOAuth2UseCustomURLChange);
|
||||||
}
|
}
|
||||||
// Edit authentication
|
// Edit authentication
|
||||||
if ($('.admin.edit.authentication').length > 0) {
|
if ($('.admin.edit.authentication').length > 0) {
|
||||||
var authType = $('#auth_type').val();
|
var authType = $('#auth_type').val();
|
||||||
if (authType == '2' || authType == '5') {
|
if (authType == '2' || authType == '5') {
|
||||||
$('#security_protocol').change(onSecurityProtocolChange);
|
$('#security_protocol').change(onSecurityProtocolChange);
|
||||||
|
} else if (authType == '6') {
|
||||||
|
$('#oauth2_provider').change(onOAuth2Change);
|
||||||
|
$('#oauth2_use_custom_url').change(onOAuth2UseCustomURLChange);
|
||||||
|
onOAuth2Change();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
margin-right: 5px;
|
margin-right: 3px;
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,9 @@
|
||||||
img {
|
img {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
&.openidConnect {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,16 +7,17 @@ package admin
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/Unknwon/com"
|
|
||||||
"github.com/go-xorm/core"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/auth"
|
"code.gitea.io/gitea/modules/auth"
|
||||||
"code.gitea.io/gitea/modules/auth/ldap"
|
"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/base"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
|
"github.com/Unknwon/com"
|
||||||
|
"github.com/go-xorm/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -77,6 +78,7 @@ func NewAuthSource(ctx *context.Context) {
|
||||||
ctx.Data["SecurityProtocols"] = securityProtocols
|
ctx.Data["SecurityProtocols"] = securityProtocols
|
||||||
ctx.Data["SMTPAuths"] = models.SMTPAuths
|
ctx.Data["SMTPAuths"] = models.SMTPAuths
|
||||||
ctx.Data["OAuth2Providers"] = models.OAuth2Providers
|
ctx.Data["OAuth2Providers"] = models.OAuth2Providers
|
||||||
|
ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
|
||||||
|
|
||||||
// only the first as default
|
// only the first as default
|
||||||
for key := range models.OAuth2Providers {
|
for key := range models.OAuth2Providers {
|
||||||
|
@ -123,10 +125,23 @@ func parseSMTPConfig(form auth.AuthenticationForm) *models.SMTPConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOAuth2Config(form auth.AuthenticationForm) *models.OAuth2Config {
|
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{
|
return &models.OAuth2Config{
|
||||||
Provider: form.Oauth2Provider,
|
Provider: form.Oauth2Provider,
|
||||||
ClientID: form.Oauth2Key,
|
ClientID: form.Oauth2Key,
|
||||||
ClientSecret: form.Oauth2Secret,
|
ClientSecret: form.Oauth2Secret,
|
||||||
|
OpenIDConnectAutoDiscoveryURL: form.OpenIDConnectAutoDiscoveryURL,
|
||||||
|
CustomURLMapping: customURLMapping,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,6 +157,7 @@ func NewAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) {
|
||||||
ctx.Data["SecurityProtocols"] = securityProtocols
|
ctx.Data["SecurityProtocols"] = securityProtocols
|
||||||
ctx.Data["SMTPAuths"] = models.SMTPAuths
|
ctx.Data["SMTPAuths"] = models.SMTPAuths
|
||||||
ctx.Data["OAuth2Providers"] = models.OAuth2Providers
|
ctx.Data["OAuth2Providers"] = models.OAuth2Providers
|
||||||
|
ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
|
||||||
|
|
||||||
hasTLS := false
|
hasTLS := false
|
||||||
var config core.Conversion
|
var config core.Conversion
|
||||||
|
@ -199,6 +215,7 @@ func EditAuthSource(ctx *context.Context) {
|
||||||
ctx.Data["SecurityProtocols"] = securityProtocols
|
ctx.Data["SecurityProtocols"] = securityProtocols
|
||||||
ctx.Data["SMTPAuths"] = models.SMTPAuths
|
ctx.Data["SMTPAuths"] = models.SMTPAuths
|
||||||
ctx.Data["OAuth2Providers"] = models.OAuth2Providers
|
ctx.Data["OAuth2Providers"] = models.OAuth2Providers
|
||||||
|
ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
|
||||||
|
|
||||||
source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
|
source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -222,6 +239,7 @@ func EditAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) {
|
||||||
|
|
||||||
ctx.Data["SMTPAuths"] = models.SMTPAuths
|
ctx.Data["SMTPAuths"] = models.SMTPAuths
|
||||||
ctx.Data["OAuth2Providers"] = models.OAuth2Providers
|
ctx.Data["OAuth2Providers"] = models.OAuth2Providers
|
||||||
|
ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
|
||||||
|
|
||||||
source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
|
source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -257,7 +275,12 @@ func EditAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) {
|
||||||
source.IsActived = form.IsActive
|
source.IsActived = form.IsActive
|
||||||
source.Cfg = config
|
source.Cfg = config
|
||||||
if err := models.UpdateSource(source); err != nil {
|
if err := models.UpdateSource(source); err != nil {
|
||||||
|
if models.IsErrOpenIDConnectInitialize(err) {
|
||||||
|
ctx.Flash.Error(err.Error(), true)
|
||||||
|
ctx.HTML(200, tplAuthEdit)
|
||||||
|
} else {
|
||||||
ctx.Handle(500, "UpdateSource", err)
|
ctx.Handle(500, "UpdateSource", err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Trace("Authentication changed by admin(%s): %d", ctx.User.Name, source.ID)
|
log.Trace("Authentication changed by admin(%s): %d", ctx.User.Name, source.ID)
|
||||||
|
|
|
@ -107,17 +107,19 @@ func checkAutoLogin(ctx *context.Context) bool {
|
||||||
|
|
||||||
// SignIn render sign in page
|
// SignIn render sign in page
|
||||||
func SignIn(ctx *context.Context) {
|
func SignIn(ctx *context.Context) {
|
||||||
|
ctx.Data["Title"] = ctx.Tr("sign_in")
|
||||||
|
|
||||||
// Check auto-login.
|
// Check auto-login.
|
||||||
if checkAutoLogin(ctx) {
|
if checkAutoLogin(ctx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
oauth2Providers, err := models.GetActiveOAuth2Providers()
|
orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Handle(500, "UserSignIn", err)
|
ctx.Handle(500, "UserSignIn", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
ctx.Data["OrderedOAuth2Names"] = orderedOAuth2Names
|
||||||
ctx.Data["OAuth2Providers"] = oauth2Providers
|
ctx.Data["OAuth2Providers"] = oauth2Providers
|
||||||
ctx.Data["Title"] = ctx.Tr("sign_in")
|
ctx.Data["Title"] = ctx.Tr("sign_in")
|
||||||
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
|
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
|
||||||
|
@ -129,11 +131,14 @@ func SignIn(ctx *context.Context) {
|
||||||
|
|
||||||
// SignInPost response for sign in request
|
// SignInPost response for sign in request
|
||||||
func SignInPost(ctx *context.Context, form auth.SignInForm) {
|
func SignInPost(ctx *context.Context, form auth.SignInForm) {
|
||||||
oauth2Providers, err := models.GetActiveOAuth2Providers()
|
ctx.Data["Title"] = ctx.Tr("sign_in")
|
||||||
|
|
||||||
|
orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Handle(500, "UserSignIn", err)
|
ctx.Handle(500, "UserSignIn", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
ctx.Data["OrderedOAuth2Names"] = orderedOAuth2Names
|
||||||
ctx.Data["OAuth2Providers"] = oauth2Providers
|
ctx.Data["OAuth2Providers"] = oauth2Providers
|
||||||
ctx.Data["Title"] = ctx.Tr("sign_in")
|
ctx.Data["Title"] = ctx.Tr("sign_in")
|
||||||
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
|
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
|
||||||
|
|
|
@ -164,6 +164,39 @@
|
||||||
<label for="oauth2_secret">{{.i18n.Tr "admin.auths.oauth2_clientSecret"}}</label>
|
<label for="oauth2_secret">{{.i18n.Tr "admin.auths.oauth2_clientSecret"}}</label>
|
||||||
<input id="oauth2_secret" name="oauth2_secret" value="{{$cfg.ClientSecret}}" required>
|
<input id="oauth2_secret" name="oauth2_secret" value="{{$cfg.ClientSecret}}" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="open_id_connect_auto_discovery_url required field">
|
||||||
|
<label for="open_id_connect_auto_discovery_url">{{.i18n.Tr "admin.auths.openIdConnectAutoDiscoveryURL"}}</label>
|
||||||
|
<input id="open_id_connect_auto_discovery_url" name="open_id_connect_auto_discovery_url" value="{{$cfg.OpenIDConnectAutoDiscoveryURL}}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="oauth2_use_custom_url inline field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<label><strong>{{.i18n.Tr "admin.auths.oauth2_use_custom_url"}}</strong></label>
|
||||||
|
<input id="oauth2_use_custom_url" name="oauth2_use_custom_url" type="checkbox" {{if $cfg.CustomURLMapping}}checked{{end}}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="oauth2_use_custom_url_field oauth2_auth_url required field">
|
||||||
|
<label for="oauth2_auth_url">{{.i18n.Tr "admin.auths.oauth2_authURL"}}</label>
|
||||||
|
<input id="oauth2_auth_url" name="oauth2_auth_url" value="{{if $cfg.CustomURLMapping}}{{$cfg.CustomURLMapping.AuthURL}}v{{end}}">
|
||||||
|
</div>
|
||||||
|
<div class="oauth2_use_custom_url_field oauth2_token_url required field">
|
||||||
|
<label for="oauth2_token_url">{{.i18n.Tr "admin.auths.oauth2_tokenURL"}}</label>
|
||||||
|
<input id="oauth2_token_url" name="oauth2_token_url" value="{{if $cfg.CustomURLMapping}}{{$cfg.CustomURLMapping.TokenURL}}{{end}}">
|
||||||
|
</div>
|
||||||
|
<div class="oauth2_use_custom_url_field oauth2_profile_url required field">
|
||||||
|
<label for="oauth2_profile_url">{{.i18n.Tr "admin.auths.oauth2_profileURL"}}</label>
|
||||||
|
<input id="oauth2_profile_url" name="oauth2_profile_url" value="{{if $cfg.CustomURLMapping}}{{$cfg.CustomURLMapping.ProfileURL}}{{end}}">
|
||||||
|
</div>
|
||||||
|
<div class="oauth2_use_custom_url_field oauth2_email_url required field">
|
||||||
|
<label for="oauth2_email_url">{{.i18n.Tr "admin.auths.oauth2_emailURL"}}</label>
|
||||||
|
<input id="oauth2_email_url" name="oauth2_email_url" value="{{if $cfg.CustomURLMapping}}{{$cfg.CustomURLMapping.EmailURL}}{{end}}">
|
||||||
|
</div>
|
||||||
|
{{if .OAuth2DefaultCustomURLMappings}}{{range $key, $value := .OAuth2DefaultCustomURLMappings}}
|
||||||
|
<input id="{{$key}}_token_url" value="{{$value.TokenURL}}" type="hidden" />
|
||||||
|
<input id="{{$key}}_auth_url" value="{{$value.AuthURL}}" type="hidden" />
|
||||||
|
<input id="{{$key}}_profile_url" value="{{$value.ProfileURL}}" type="hidden" />
|
||||||
|
<input id="{{$key}}_email_url" value="{{$value.EmailURL}}" type="hidden" />
|
||||||
|
{{end}}{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="inline field {{if not .Source.IsSMTP}}hide{{end}}">
|
<div class="inline field {{if not .Source.IsSMTP}}hide{{end}}">
|
||||||
|
|
|
@ -80,8 +80,29 @@
|
||||||
<div class="ui attached segment">
|
<div class="ui attached segment">
|
||||||
<h5>GMail Settings:</h5>
|
<h5>GMail Settings:</h5>
|
||||||
<p>Host: smtp.gmail.com, Port: 587, Enable TLS Encryption: true</p>
|
<p>Host: smtp.gmail.com, Port: 587, Enable TLS Encryption: true</p>
|
||||||
<h5>OAuth GitHub:</h5>
|
|
||||||
<p>{{.i18n.Tr "admin.auths.tip.github"}}</p>
|
<h5>{{.i18n.Tr "admin.auths.tips.oauth2.general"}}:</h5>
|
||||||
|
<p>{{.i18n.Tr "admin.auths.tips.oauth2.general.tip"}}</p>
|
||||||
|
|
||||||
|
<h5 class="ui top attached header">{{.i18n.Tr "admin.auths.tip.oauth2_provider"}}</h5>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<li>Bitbucket</li>
|
||||||
|
<span>{{.i18n.Tr "admin.auths.tip.bitbucket"}}</span>
|
||||||
|
<li>Dropbox</li>
|
||||||
|
<span>{{.i18n.Tr "admin.auths.tip.dropbox"}}</span>
|
||||||
|
<li>Facebook</li>
|
||||||
|
<span>{{.i18n.Tr "admin.auths.tip.facebook"}}</span>
|
||||||
|
<li>GitHub</li>
|
||||||
|
<span>{{.i18n.Tr "admin.auths.tip.github"}}</span>
|
||||||
|
<li>GitLab</li>
|
||||||
|
<span>{{.i18n.Tr "admin.auths.tip.gitlab"}}</span>
|
||||||
|
<li>Google+</li>
|
||||||
|
<span>{{.i18n.Tr "admin.auths.tip.google_plus"}}</span>
|
||||||
|
<li>OpenID Connect</li>
|
||||||
|
<span>{{.i18n.Tr "admin.auths.tip.openid_connect"}}</span>
|
||||||
|
<li>Twitter</li>
|
||||||
|
<span>{{.i18n.Tr "admin.auths.tip.twitter"}}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -20,4 +20,39 @@
|
||||||
<label for="oauth2_secret">{{.i18n.Tr "admin.auths.oauth2_clientSecret"}}</label>
|
<label for="oauth2_secret">{{.i18n.Tr "admin.auths.oauth2_clientSecret"}}</label>
|
||||||
<input id="oauth2_secret" name="oauth2_secret" value="{{.oauth2_secret}}">
|
<input id="oauth2_secret" name="oauth2_secret" value="{{.oauth2_secret}}">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="open_id_connect_auto_discovery_url required field">
|
||||||
|
<label for="open_id_connect_auto_discovery_url">{{.i18n.Tr "admin.auths.openIdConnectAutoDiscoveryURL"}}</label>
|
||||||
|
<input id="open_id_connect_auto_discovery_url" name="open_id_connect_auto_discovery_url" value="{{.open_id_connect_auto_discovery_url}}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="oauth2_use_custom_url inline field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<label><strong>{{.i18n.Tr "admin.auths.oauth2_use_custom_url"}}</strong></label>
|
||||||
|
<input id="oauth2_use_custom_url" name="oauth2_use_custom_url" type="checkbox">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="oauth2_use_custom_url_field oauth2_auth_url required field">
|
||||||
|
<label for="oauth2_auth_url">{{.i18n.Tr "admin.auths.oauth2_authURL"}}</label>
|
||||||
|
<input id="oauth2_auth_url" name="oauth2_auth_url" value="{{.oauth2_auth_url}}">
|
||||||
|
</div>
|
||||||
|
<div class="oauth2_use_custom_url_field oauth2_token_url required field">
|
||||||
|
<label for="oauth2_token_url">{{.i18n.Tr "admin.auths.oauth2_tokenURL"}}</label>
|
||||||
|
<input id="oauth2_token_url" name="oauth2_token_url" value="{{.oauth2_token_url}}">
|
||||||
|
</div>
|
||||||
|
<div class="oauth2_use_custom_url_field oauth2_profile_url required field">
|
||||||
|
<label for="oauth2_profile_url">{{.i18n.Tr "admin.auths.oauth2_profileURL"}}</label>
|
||||||
|
<input id="oauth2_profile_url" name="oauth2_profile_url" value="{{.oauth2_profile_url}}">
|
||||||
|
</div>
|
||||||
|
<div class="oauth2_use_custom_url_field oauth2_email_url required field">
|
||||||
|
<label for="oauth2_email_url">{{.i18n.Tr "admin.auths.oauth2_emailURL"}}</label>
|
||||||
|
<input id="oauth2_email_url" name="oauth2_email_url" value="{{.oauth2_email_url}}">
|
||||||
|
</div>
|
||||||
|
{{if .OAuth2DefaultCustomURLMappings}}
|
||||||
|
{{range $key, $value := .OAuth2DefaultCustomURLMappings}}
|
||||||
|
<input id="{{$key}}_token_url" value="{{$value.TokenURL}}" type="hidden" />
|
||||||
|
<input id="{{$key}}_auth_url" value="{{$value.AuthURL}}" type="hidden" />
|
||||||
|
<input id="{{$key}}_profile_url" value="{{$value.ProfileURL}}" type="hidden" />
|
||||||
|
<input id="{{$key}}_email_url" value="{{$value.EmailURL}}" type="hidden" />
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
|
@ -8,6 +8,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ui user signin container icon">
|
||||||
{{template "user/auth/signin_inner" .}}
|
{{template "user/auth/signin_inner" .}}
|
||||||
|
</div>
|
||||||
{{template "user/auth/signup_inner" .}}
|
{{template "user/auth/signup_inner" .}}
|
||||||
{{template "base/footer" .}}
|
{{template "base/footer" .}}
|
||||||
|
|
|
@ -38,11 +38,14 @@
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .OAuth2Providers}}
|
{{if and .OrderedOAuth2Names .OAuth2Providers}}
|
||||||
<div class="ui attached segment">
|
<div class="ui attached segment">
|
||||||
<div class="oauth2 center">
|
<div class="oauth2 center">
|
||||||
<div>
|
<div>
|
||||||
<p>{{.i18n.Tr "sign_in_with"}}</p>{{range $key, $value := .OAuth2Providers}}<a href="{{AppSubUrl}}/user/oauth2/{{$key}}"><img alt="{{$value.DisplayName}}" title="{{$value.DisplayName}}" src="{{AppSubUrl}}{{$value.Image}}"></a>{{end}}
|
<p>{{.i18n.Tr "sign_in_with"}}</p>{{range $key := .OrderedOAuth2Names}}
|
||||||
|
{{$provider := index $.OAuth2Providers $key}}
|
||||||
|
<a href="{{AppSubUrl}}/user/oauth2/{{$key}}"><img alt="{{$provider.DisplayName}}{{if eq $provider.Name "openidConnect"}} ({{$key}}){{end}}" title="{{$provider.DisplayName}}{{if eq $provider.Name "openidConnect"}} ({{$key}}){{end}}" class="{{$provider.Name}}" src="{{AppSubUrl}}{{$provider.Image}}"></a>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
206
vendor/github.com/markbates/goth/providers/bitbucket/bitbucket.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -37,13 +37,20 @@ var (
|
||||||
// You should always call `github.New` to get a new Provider. Never try to create
|
// You should always call `github.New` to get a new Provider. Never try to create
|
||||||
// one manually.
|
// one manually.
|
||||||
func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
|
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{
|
p := &Provider{
|
||||||
ClientKey: clientKey,
|
ClientKey: clientKey,
|
||||||
Secret: secret,
|
Secret: secret,
|
||||||
CallbackURL: callbackURL,
|
CallbackURL: callbackURL,
|
||||||
providerName: "github",
|
providerName: "github",
|
||||||
|
profileURL: profileURL,
|
||||||
|
emailURL: emailURL,
|
||||||
}
|
}
|
||||||
p.config = newConfig(p, scopes)
|
p.config = newConfig(p, authURL, tokenURL, scopes)
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,6 +62,8 @@ type Provider struct {
|
||||||
HTTPClient *http.Client
|
HTTPClient *http.Client
|
||||||
config *oauth2.Config
|
config *oauth2.Config
|
||||||
providerName string
|
providerName string
|
||||||
|
profileURL string
|
||||||
|
emailURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name is the name used to retrieve this provider later.
|
// 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)
|
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 err != nil {
|
||||||
return user, err
|
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) {
|
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 err != nil {
|
||||||
if response != nil {
|
if response != nil {
|
||||||
response.Body.Close()
|
response.Body.Close()
|
||||||
|
@ -194,14 +203,14 @@ func getPrivateMail(p *Provider, sess *Session) (email string, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func newConfig(provider *Provider, scopes []string) *oauth2.Config {
|
func newConfig(provider *Provider, authURL, tokenURL string, scopes []string) *oauth2.Config {
|
||||||
c := &oauth2.Config{
|
c := &oauth2.Config{
|
||||||
ClientID: provider.ClientKey,
|
ClientID: provider.ClientKey,
|
||||||
ClientSecret: provider.Secret,
|
ClientSecret: provider.Secret,
|
||||||
RedirectURL: provider.CallbackURL,
|
RedirectURL: provider.CallbackURL,
|
||||||
Endpoint: oauth2.Endpoint{
|
Endpoint: oauth2.Endpoint{
|
||||||
AuthURL: AuthURL,
|
AuthURL: authURL,
|
||||||
TokenURL: TokenURL,
|
TokenURL: tokenURL,
|
||||||
},
|
},
|
||||||
Scopes: []string{},
|
Scopes: []string{},
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,187 @@
|
||||||
|
// 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
|
||||||
|
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",
|
||||||
|
profileURL: profileURL,
|
||||||
|
}
|
||||||
|
p.config = newConfig(p, authURL, tokenURL, 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(p.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, 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,
|
||||||
|
},
|
||||||
|
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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,195 @@
|
||||||
|
// Package gplus implements the OAuth2 protocol for authenticating users through Google+.
|
||||||
|
// This package can be used as a reference implementation of an OAuth2 provider for Goth.
|
||||||
|
package gplus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/markbates/goth"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
authURL string = "https://accounts.google.com/o/oauth2/auth?access_type=offline"
|
||||||
|
tokenURL string = "https://accounts.google.com/o/oauth2/token"
|
||||||
|
endpointProfile string = "https://www.googleapis.com/oauth2/v2/userinfo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New creates a new Google+ provider, and sets up important connection details.
|
||||||
|
// You should always call `gplus.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: "gplus",
|
||||||
|
}
|
||||||
|
p.config = newConfig(p, scopes)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider is the implementation of `goth.Provider` for accessing Google+.
|
||||||
|
type Provider struct {
|
||||||
|
ClientKey string
|
||||||
|
Secret string
|
||||||
|
CallbackURL string
|
||||||
|
HTTPClient *http.Client
|
||||||
|
config *oauth2.Config
|
||||||
|
prompt oauth2.AuthCodeOption
|
||||||
|
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 gplus package.
|
||||||
|
func (p *Provider) Debug(debug bool) {}
|
||||||
|
|
||||||
|
// BeginAuth asks Google+ for an authentication end-point.
|
||||||
|
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
|
||||||
|
var opts []oauth2.AuthCodeOption
|
||||||
|
if p.prompt != nil {
|
||||||
|
opts = append(opts, p.prompt)
|
||||||
|
}
|
||||||
|
url := p.config.AuthCodeURL(state, opts...)
|
||||||
|
session := &Session{
|
||||||
|
AuthURL: url,
|
||||||
|
}
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchUser will go to Google+ 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(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)
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func userFromReader(reader io.Reader, user *goth.User) error {
|
||||||
|
u := struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
FirstName string `json:"given_name"`
|
||||||
|
LastName string `json:"family_name"`
|
||||||
|
Link string `json:"link"`
|
||||||
|
Picture string `json:"picture"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
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.Bio
|
||||||
|
user.AvatarURL = u.Picture
|
||||||
|
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{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(scopes) > 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, " "))
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
384
vendor/github.com/markbates/goth/providers/openidConnect/openidConnect.go
generated
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
63
vendor/github.com/markbates/goth/providers/openidConnect/session.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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.
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -0,0 +1,163 @@
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
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
|
||||||
|
}
|
|
@ -542,20 +542,62 @@
|
||||||
{
|
{
|
||||||
"checksumSHA1": "O3KUfEXQPfdQ+tCMpP2RAIRJJqY=",
|
"checksumSHA1": "O3KUfEXQPfdQ+tCMpP2RAIRJJqY=",
|
||||||
"path": "github.com/markbates/goth",
|
"path": "github.com/markbates/goth",
|
||||||
"revision": "450379d2950a65070b23cc93c53436553add4484",
|
"revision": "90362394a367f9d77730911973462a53d69662ba",
|
||||||
"revisionTime": "2017-02-06T19:46:32Z"
|
"revisionTime": "2017-02-23T14:12:10Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "MkFKwLV3icyUo4oP0BgEs+7+R1Y=",
|
"checksumSHA1": "MkFKwLV3icyUo4oP0BgEs+7+R1Y=",
|
||||||
"path": "github.com/markbates/goth/gothic",
|
"path": "github.com/markbates/goth/gothic",
|
||||||
"revision": "450379d2950a65070b23cc93c53436553add4484",
|
"revision": "90362394a367f9d77730911973462a53d69662ba",
|
||||||
"revisionTime": "2017-02-06T19:46:32Z"
|
"revisionTime": "2017-02-23T14:12:10Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "ZFqznX3/ZW65I4QeepiHQdE69nA=",
|
"checksumSHA1": "crNSlQADjX6hcxykON2tFCqY4iw=",
|
||||||
|
"path": "github.com/markbates/goth/providers/bitbucket",
|
||||||
|
"revision": "90362394a367f9d77730911973462a53d69662ba",
|
||||||
|
"revisionTime": "2017-02-23T14:12:10Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "1Kp4DKkJNVn135Xg8H4a6CFBNy8=",
|
||||||
|
"path": "github.com/markbates/goth/providers/dropbox",
|
||||||
|
"revision": "90362394a367f9d77730911973462a53d69662ba",
|
||||||
|
"revisionTime": "2017-02-23T14:12:10Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "cGs1da29iOBJh5EAH0icKDbN8CA=",
|
||||||
|
"path": "github.com/markbates/goth/providers/facebook",
|
||||||
|
"revision": "90362394a367f9d77730911973462a53d69662ba",
|
||||||
|
"revisionTime": "2017-02-23T14:12:10Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "P6nBZ850aaekpOcoXNdRhK86bH8=",
|
||||||
"path": "github.com/markbates/goth/providers/github",
|
"path": "github.com/markbates/goth/providers/github",
|
||||||
"revision": "450379d2950a65070b23cc93c53436553add4484",
|
"revision": "90362394a367f9d77730911973462a53d69662ba",
|
||||||
"revisionTime": "2017-02-06T19:46:32Z"
|
"revisionTime": "2017-02-23T14:12:10Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "o/109paSRy9HqV87gR4zUZMMSzs=",
|
||||||
|
"path": "github.com/markbates/goth/providers/gitlab",
|
||||||
|
"revision": "90362394a367f9d77730911973462a53d69662ba",
|
||||||
|
"revisionTime": "2017-02-23T14:12:10Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "cX6kR9y94BWFZvI/7UFrsFsP3FQ=",
|
||||||
|
"path": "github.com/markbates/goth/providers/gplus",
|
||||||
|
"revision": "90362394a367f9d77730911973462a53d69662ba",
|
||||||
|
"revisionTime": "2017-02-23T14:12:10Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "sMYKhqAUZXM1+T/TjlMhWh8Vveo=",
|
||||||
|
"path": "github.com/markbates/goth/providers/openidConnect",
|
||||||
|
"revision": "90362394a367f9d77730911973462a53d69662ba",
|
||||||
|
"revisionTime": "2017-02-23T14:12:10Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "1w0V6jYXaGlEtZcMeYTOAAucvgw=",
|
||||||
|
"path": "github.com/markbates/goth/providers/twitter",
|
||||||
|
"revision": "90362394a367f9d77730911973462a53d69662ba",
|
||||||
|
"revisionTime": "2017-02-23T14:12:10Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "9FJUwn3EIgASVki+p8IHgWVC5vQ=",
|
"checksumSHA1": "9FJUwn3EIgASVki+p8IHgWVC5vQ=",
|
||||||
|
@ -575,6 +617,12 @@
|
||||||
"revision": "f77f16ffc87a6a58814e64ae72d55f9c41374e6d",
|
"revision": "f77f16ffc87a6a58814e64ae72d55f9c41374e6d",
|
||||||
"revisionTime": "2016-10-12T08:37:05Z"
|
"revisionTime": "2016-10-12T08:37:05Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "hQcIDtbilIlkJaYhl2faWIFL8uY=",
|
||||||
|
"path": "github.com/mrjones/oauth",
|
||||||
|
"revision": "3f67d9c274355678b2f9844b08d643e2f9213340",
|
||||||
|
"revisionTime": "2017-02-25T17:57:52Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "lfOuMiAdiqc/dalUSBTvD5ZMSzA=",
|
"checksumSHA1": "lfOuMiAdiqc/dalUSBTvD5ZMSzA=",
|
||||||
"path": "github.com/msteinert/pam",
|
"path": "github.com/msteinert/pam",
|
||||||
|
|